diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..52ac3648b0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: +patreon: ottowinter +open_collective: +ko_fi: +tidelift: +custom: https://esphome.io/guides/supporters.html diff --git a/.gitignore b/.gitignore index 6002612c13..b004947390 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ venv.bak/ .pioenvs .piolibdeps +.pio .vscode CMakeListsPrivate.txt CMakeLists.txt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3278827486..3db0b982ae 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ variables: DOCKER_DRIVER: overlay2 DOCKER_HOST: tcp://docker:2375/ - BASE_VERSION: '1.8.3' + BASE_VERSION: '2.0.1' TZ: UTC stages: @@ -33,7 +33,7 @@ stages: - docker info - docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" script: - - docker run --rm --privileged hassioaddons/qemu-user-static:latest + - docker run --rm --privileged multiarch/qemu-user-static:4.1.0-1 --reset -p yes - TAG="${CI_COMMIT_TAG#v}" - TAG="${TAG:-${CI_COMMIT_SHA:0:7}}" - echo "Tag ${TAG}" @@ -107,10 +107,6 @@ lint-tidy: <<: *lint script: - pio init --ide atom - - | - if ! patch -R -p0 -s -f --dry-run <script/.neopixelbus.patch; then - patch -p0 < script/.neopixelbus.patch - fi - script/clang-tidy --all-headers --fix - script/ci-suggest-changes diff --git a/.travis.yml b/.travis.yml index dba512cb1f..aa848877c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,26 @@ sudo: false language: python -python: '2.7' +python: '3.5' install: script/setup cache: directories: - "~/.platformio" - - "$TRAVIS_BUILD_DIR/.piolibdeps" - - "$TRAVIS_BUILD_DIR/tests/build/test1/.piolibdeps" - - "$TRAVIS_BUILD_DIR/tests/build/test2/.piolibdeps" - - "$TRAVIS_BUILD_DIR/tests/build/test3/.piolibdeps" matrix: fast_finish: true include: - - python: "2.7" - env: TARGET=Lint2.7 + - python: "3.7" + env: TARGET=Lint3.7 script: - script/ci-custom.py - flake8 esphome - pylint esphome - - python: "3.5.3" - env: TARGET=Lint3.5 + - python: "3.5" + env: TARGET=Test3.5 script: - - script/ci-custom.py - - flake8 esphome - - pylint esphome + - esphome tests/test1.yaml compile + - esphome tests/test2.yaml compile + - esphome tests/test3.yaml compile - python: "2.7" env: TARGET=Test2.7 script: @@ -44,10 +40,6 @@ matrix: - clang-format-7 before_script: - pio init --ide atom - - | - if ! patch -R -p0 -s -f --dry-run <script/.neopixelbus.patch; then - patch -p0 < script/.neopixelbus.patch - fi - clang-tidy-7 -version - clang-format-7 -version - clang-apply-replacements-7 -version diff --git a/MANIFEST.in b/MANIFEST.in index f4a2cc672d..cdea2df2a6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include LICENSE include README.md include esphome/dashboard/templates/*.html -recursive-include esphome/dashboard/static *.ico *.js *.css +recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE recursive-include esphome *.cpp *.h *.tcc +recursive-include esphome LICENSE.txt diff --git a/docker/Dockerfile b/docker/Dockerfile index a5edeeec00..11bbeeda2b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,8 +1,11 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:1.8.3 +ARG BUILD_FROM=esphome/esphome-base-amd64:2.0.1 FROM ${BUILD_FROM} COPY . . -RUN pip2 install --no-cache-dir -e . +RUN pip3 install --no-cache-dir -e . + +ENV USERNAME="" +ENV PASSWORD="" WORKDIR /config ENTRYPOINT ["esphome"] diff --git a/docker/Dockerfile.hassio b/docker/Dockerfile.hassio index 4d3d0b88f1..e5c9625680 100644 --- a/docker/Dockerfile.hassio +++ b/docker/Dockerfile.hassio @@ -6,7 +6,7 @@ COPY docker/rootfs/ / COPY setup.py setup.cfg MANIFEST.in /opt/esphome/ COPY esphome /opt/esphome/esphome -RUN pip2 install --no-cache-dir -e /opt/esphome +RUN pip3 install --no-cache-dir -e /opt/esphome # Build arguments ARG BUILD_VERSION=dev diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index 2cc65b85d2..5d8893bdbe 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -1,4 +1,4 @@ -FROM esphome/esphome-base-amd64:1.8.3 +FROM esphome/esphome-base-amd64:2.0.1 RUN \ apt-get update \ @@ -12,7 +12,9 @@ RUN \ /var/lib/apt/lists/* COPY requirements_test.txt /requirements_test.txt -RUN pip2 install --no-cache-dir wheel && pip2 install --no-cache-dir -r /requirements_test.txt +RUN pip3 install --no-cache-dir wheel && pip3 install --no-cache-dir -r /requirements_test.txt + +RUN ln -s /usr/bin/pip3 /usr/bin/pip && ln -f -s /usr/bin/python3 /usr/bin/python VOLUME ["/esphome"] WORKDIR /esphome diff --git a/docker/rootfs/etc/cont-init.d/30-esphome.sh b/docker/rootfs/etc/cont-init.d/30-esphome.sh index 8fc1b472c0..086c5af19f 100644 --- a/docker/rootfs/etc/cont-init.d/30-esphome.sh +++ b/docker/rootfs/etc/cont-init.d/30-esphome.sh @@ -10,6 +10,6 @@ if bashio::config.has_value 'esphome_version'; then esphome_version=$(bashio::config 'esphome_version') full_url="https://github.com/esphome/esphome/archive/${esphome_version}.zip" bashio::log.info "Installing esphome version '${esphome_version}' (${full_url})..." - pip2 install -U --no-cache-dir "${full_url}" \ + pip3 install -U --no-cache-dir "${full_url}" \ || bashio::exit.nok "Failed installing esphome pinned version." fi diff --git a/esphome/__main__.py b/esphome/__main__.py index c48d49b9ac..a78413ff04 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -11,11 +11,11 @@ from esphome import const, writer, yaml_util import esphome.codegen as cg from esphome.config import iter_components, read_config, strip_default_ids from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \ - CONF_PASSWORD, CONF_PORT + CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority from esphome.helpers import color, indent from esphome.py_compat import IS_PY2, safe_input -from esphome.util import run_external_command, run_external_process, safe_print +from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files _LOGGER = logging.getLogger(__name__) @@ -24,7 +24,7 @@ def get_serial_ports(): # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py from serial.tools.list_ports import comports result = [] - for port, desc, info in comports(): + for port, desc, info in comports(include_links=True): if not port: continue if "VID:PID" in info: @@ -35,7 +35,9 @@ def get_serial_ports(): def choose_prompt(options): if not options: - raise ValueError + raise EsphomeError("Found no valid options for upload/logging, please make sure relevant " + "sections (ota, mqtt, ...) are in your configuration and/or the device " + "is plugged in.") if len(options) == 1: return options[0][1] @@ -130,6 +132,7 @@ def wrap_to_code(name, comp): conf_str = yaml_util.dump(conf) if IS_PY2: conf_str = conf_str.decode('utf-8') + conf_str = conf_str.replace('//', '') cg.add(cg.LineComment(indent(conf_str))) yield coro(conf) @@ -157,12 +160,13 @@ def compile_program(args, config): from esphome import platformio_api _LOGGER.info("Compiling app...") - return platformio_api.run_compile(config, args.verbose) + return platformio_api.run_compile(config, CORE.verbose) def upload_using_esptool(config, port): path = CORE.firmware_bin cmd = ['esptool.py', '--before', 'default_reset', '--after', 'hard_reset', + '--baud', str(config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get('upload_speed', 460800)), '--chip', 'esp8266', '--port', port, 'write_flash', '0x0', path] if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: @@ -180,15 +184,14 @@ def upload_program(config, args, host): if CORE.is_esp8266: return upload_using_esptool(config, host) - return platformio_api.run_upload(config, args.verbose, host) + return platformio_api.run_upload(config, CORE.verbose, host) from esphome import espota2 ota_conf = config[CONF_OTA] remote_port = ota_conf[CONF_PORT] password = ota_conf[CONF_PASSWORD] - res = espota2.run_ota(host, remote_port, password, CORE.firmware_bin) - return res + return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) def show_logs(config, args, port): @@ -218,6 +221,7 @@ def clean_mqtt(config, args): def setup_log(debug=False, quiet=False): if debug: log_level = logging.DEBUG + CORE.verbose = True elif quiet: log_level = logging.CRITICAL else: @@ -250,12 +254,12 @@ def setup_log(debug=False, quiet=False): def command_wizard(args): from esphome import wizard - return wizard.wizard(args.configuration) + return wizard.wizard(args.configuration[0]) def command_config(args, config): _LOGGER.info("Configuration is valid!") - if not args.verbose: + if not CORE.verbose: config = strip_default_ids(config) safe_print(yaml_util.dump(config)) return 0 @@ -264,7 +268,7 @@ def command_config(args, config): def command_vscode(args): from esphome import vscode - CORE.config_path = args.configuration + CORE.config_path = args.configuration[0] vscode.read_config(args) @@ -350,11 +354,52 @@ def command_dashboard(args): return dashboard.start_web_server(args) +def command_update_all(args): + import click + + success = {} + files = list_yaml_files(args.configuration[0]) + twidth = 60 + + def print_bar(middle_text): + middle_text = " {} ".format(middle_text) + width = len(click.unstyle(middle_text)) + half_line = "=" * ((twidth - width) // 2) + click.echo("%s%s%s" % (half_line, middle_text, half_line)) + + for f in files: + print("Updating {}".format(color('cyan', f))) + print('-' * twidth) + print() + rc = run_external_process('esphome', '--dashboard', f, 'run', '--no-logs') + if rc == 0: + print_bar("[{}] {}".format(color('bold_green', 'SUCCESS'), f)) + success[f] = True + else: + print_bar("[{}] {}".format(color('bold_red', 'ERROR'), f)) + success[f] = False + + print() + print() + print() + + print_bar('[{}]'.format(color('bold_white', 'SUMMARY'))) + failed = 0 + for f in files: + if success[f]: + print(" - {}: {}".format(f, color('green', 'SUCCESS'))) + else: + print(" - {}: {}".format(f, color('bold_red', 'FAILED'))) + failed += 1 + return failed + + PRE_CONFIG_ACTIONS = { 'wizard': command_wizard, 'version': command_version, 'dashboard': command_dashboard, 'vscode': command_vscode, + 'update-all': command_update_all, } POST_CONFIG_ACTIONS = { @@ -370,13 +415,13 @@ POST_CONFIG_ACTIONS = { def parse_args(argv): - parser = argparse.ArgumentParser(prog='esphome') + parser = argparse.ArgumentParser(description='ESPHome v{}'.format(const.__version__)) parser.add_argument('-v', '--verbose', help="Enable verbose esphome logs.", action='store_true') parser.add_argument('-q', '--quiet', help="Disable all esphome logs.", action='store_true') parser.add_argument('--dashboard', help=argparse.SUPPRESS, action='store_true') - parser.add_argument('configuration', help='Your YAML configuration file.') + parser.add_argument('configuration', help='Your YAML configuration file.', nargs='*') subparsers = parser.add_subparsers(help='Commands', dest='command') subparsers.required = True @@ -433,7 +478,11 @@ def parse_args(argv): help="Create a simple web server for a dashboard.") dashboard.add_argument("--port", help="The HTTP port to open connections on. Defaults to 6052.", type=int, default=6052) - dashboard.add_argument("--password", help="The optional password to require for all requests.", + dashboard.add_argument("--username", help="The optional username to require " + "for authentication.", + type=str, default='') + dashboard.add_argument("--password", help="The optional password to require " + "for authentication.", type=str, default='') dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.", action='store_true') @@ -446,6 +495,8 @@ def parse_args(argv): vscode = subparsers.add_parser('vscode', help=argparse.SUPPRESS) vscode.add_argument('--ace', action='store_true') + subparsers.add_parser('update-all', help=argparse.SUPPRESS) + return parser.parse_args(argv[1:]) @@ -454,6 +505,15 @@ def run_esphome(argv): CORE.dashboard = args.dashboard setup_log(args.verbose, args.quiet) + if args.command != 'version' and not args.configuration: + _LOGGER.error("Missing configuration parameter, see esphome --help.") + return 1 + + if IS_PY2: + _LOGGER.warning("You're using ESPHome with python 2. Support for python 2 is deprecated " + "and will be removed in 1.15.0. Please reinstall ESPHome with python 3.6 " + "or higher.") + if args.command in PRE_CONFIG_ACTIONS: try: return PRE_CONFIG_ACTIONS[args.command](args) @@ -461,21 +521,28 @@ def run_esphome(argv): _LOGGER.error(e) return 1 - CORE.config_path = args.configuration + for conf_path in args.configuration: + CORE.config_path = conf_path + CORE.dashboard = args.dashboard - config = read_config(args.verbose) - if config is None: - return 1 - CORE.config = config + config = read_config() + if config is None: + return 1 + CORE.config = config + + if args.command not in POST_CONFIG_ACTIONS: + safe_print(u"Unknown command {}".format(args.command)) - if args.command in POST_CONFIG_ACTIONS: try: - return POST_CONFIG_ACTIONS[args.command](args, config) + rc = POST_CONFIG_ACTIONS[args.command](args, config) except EsphomeError as e: _LOGGER.error(e) return 1 - safe_print(u"Unknown command {}".format(args.command)) - return 1 + if rc != 0: + return rc + + CORE.reset() + return 0 def main(): diff --git a/esphome/api/api.proto b/esphome/api/api.proto deleted file mode 100644 index 37cb792959..0000000000 --- a/esphome/api/api.proto +++ /dev/null @@ -1,330 +0,0 @@ -syntax = "proto3"; - -// The Home Assistant protocol is structured as a simple -// TCP socket with short binary messages encoded in the protocol buffers format -// First, a message in this protocol has a specific format: -// * VarInt denoting the size of the message object. (type is not part of this) -// * VarInt denoting the type of message. -// * The message object encoded as a ProtoBuf message - -// The connection is established in 4 steps: -// * First, the client connects to the server and sends a "Hello Request" identifying itself -// * The server responds with a "Hello Response" and selects the protocol version -// * After receiving this message, the client attempts to authenticate itself using -// the password and a "Connect Request" -// * The server responds with a "Connect Response" and notifies of invalid password. -// If anything in this initial process fails, the connection must immediately closed -// by both sides and _no_ disconnection message is to be sent. - -// Message sent at the beginning of each connection -// Can only be sent by the client and only at the beginning of the connection -message HelloRequest { - // Description of client (like User Agent) - // For example "Home Assistant" - // Not strictly necessary to send but nice for debugging - // purposes. - string client_info = 1; -} - -// Confirmation of successful connection request. -// Can only be sent by the server and only at the beginning of the connection -message HelloResponse { - // The version of the API to use. The _client_ (for example Home Assistant) needs to check - // for compatibility and if necessary adopt to an older API. - // Major is for breaking changes in the base protocol - a mismatch will lead to immediate disconnect_client_ - // Minor is for breaking changes in individual messages - a mismatch will lead to a warning message - uint32 api_version_major = 1; - uint32 api_version_minor = 2; - - // A string identifying the server (ESP); like client info this may be empty - // and only exists for debugging/logging purposes. - // For example "ESPHome v1.10.0 on ESP8266" - string server_info = 3; -} - -// Message sent at the beginning of each connection to authenticate the client -// Can only be sent by the client and only at the beginning of the connection -message ConnectRequest { - // The password to log in with - string password = 1; -} - -// Confirmation of successful connection. After this the connection is available for all traffic. -// Can only be sent by the server and only at the beginning of the connection -message ConnectResponse { - bool invalid_password = 1; -} - -// Request to close the connection. -// Can be sent by both the client and server -message DisconnectRequest { - // Do not close the connection before the acknowledgement arrives -} - -message DisconnectResponse { - // Empty - Both parties are required to close the connection after this - // message has been received. -} - -message PingRequest { - // Empty -} - -message PingResponse { - // Empty -} - -message DeviceInfoRequest { - // Empty -} - -message DeviceInfoResponse { - bool uses_password = 1; - - // The name of the node, given by "App.set_name()" - string name = 2; - - // The mac address of the device. For example "AC:BC:32:89:0E:A9" - string mac_address = 3; - - // A string describing the ESPHome version. For example "1.10.0" - string esphome_core_version = 4; - - // A string describing the date of compilation, this is generated by the compiler - // and therefore may not be in the same format all the time. - // If the user isn't using esphome, this will also not be set. - string compilation_time = 5; - - // The model of the board. For example NodeMCU - string model = 6; - - bool has_deep_sleep = 7; -} - -message ListEntitiesRequest { - // Empty -} - -message ListEntitiesBinarySensorResponse { - string object_id = 1; - fixed32 key = 2; - string name = 3; - string unique_id = 4; - - string device_class = 5; - bool is_status_binary_sensor = 6; -} -message ListEntitiesCoverResponse { - string object_id = 1; - fixed32 key = 2; - string name = 3; - string unique_id = 4; - - bool is_optimistic = 5; -} -message ListEntitiesFanResponse { - string object_id = 1; - fixed32 key = 2; - string name = 3; - string unique_id = 4; - - bool supports_oscillation = 5; - bool supports_speed = 6; -} -message ListEntitiesLightResponse { - string object_id = 1; - fixed32 key = 2; - string name = 3; - string unique_id = 4; - - bool supports_brightness = 5; - bool supports_rgb = 6; - bool supports_white_value = 7; - bool supports_color_temperature = 8; - float min_mireds = 9; - float max_mireds = 10; - repeated string effects = 11; -} -message ListEntitiesSensorResponse { - string object_id = 1; - fixed32 key = 2; - string name = 3; - string unique_id = 4; - - string icon = 5; - string unit_of_measurement = 6; - int32 accuracy_decimals = 7; -} -message ListEntitiesSwitchResponse { - string object_id = 1; - fixed32 key = 2; - string name = 3; - string unique_id = 4; - - string icon = 5; - bool optimistic = 6; -} -message ListEntitiesTextSensorResponse { - string object_id = 1; - fixed32 key = 2; - string name = 3; - string unique_id = 4; - - string icon = 5; -} -message ListEntitiesDoneResponse { - // Empty -} - -message SubscribeStatesRequest { - // Empty -} -message BinarySensorStateResponse { - fixed32 key = 1; - bool state = 2; -} -message CoverStateResponse { - fixed32 key = 1; - enum CoverState { - OPEN = 0; - CLOSED = 1; - } - CoverState state = 2; -} -enum FanSpeed { - LOW = 0; - MEDIUM = 1; - HIGH = 2; -} -message FanStateResponse { - fixed32 key = 1; - bool state = 2; - bool oscillating = 3; - FanSpeed speed = 4; -} -message LightStateResponse { - fixed32 key = 1; - bool state = 2; - float brightness = 3; - float red = 4; - float green = 5; - float blue = 6; - float white = 7; - float color_temperature = 8; - string effect = 9; -} -message SensorStateResponse { - fixed32 key = 1; - float state = 2; -} -message SwitchStateResponse { - fixed32 key = 1; - bool state = 2; -} -message TextSensorStateResponse { - fixed32 key = 1; - string state = 2; -} - -message CoverCommandRequest { - fixed32 key = 1; - enum CoverCommand { - OPEN = 0; - CLOSE = 1; - STOP = 2; - } - bool has_state = 2; - CoverCommand command = 3; -} -message FanCommandRequest { - fixed32 key = 1; - bool has_state = 2; - bool state = 3; - bool has_speed = 4; - FanSpeed speed = 5; - bool has_oscillating = 6; - bool oscillating = 7; -} -message LightCommandRequest { - fixed32 key = 1; - bool has_state = 2; - bool state = 3; - bool has_brightness = 4; - float brightness = 5; - bool has_rgb = 6; - float red = 7; - float green = 8; - float blue = 9; - bool has_white = 10; - float white = 11; - bool has_color_temperature = 12; - float color_temperature = 13; - bool has_transition_length = 14; - uint32 transition_length = 15; - bool has_flash_length = 16; - uint32 flash_length = 17; - bool has_effect = 18; - string effect = 19; -} -message SwitchCommandRequest { - fixed32 key = 1; - bool state = 2; -} - -enum LogLevel { - NONE = 0; - ERROR = 1; - WARN = 2; - INFO = 3; - DEBUG = 4; - VERBOSE = 5; - VERY_VERBOSE = 6; -} - -message SubscribeLogsRequest { - LogLevel level = 1; - bool dump_config = 2; -} - -message SubscribeLogsResponse { - LogLevel level = 1; - string tag = 2; - string message = 3; - bool send_failed = 4; -} - -message SubscribeServiceCallsRequest { - -} - -message ServiceCallResponse { - string service = 1; - map<string, string> data = 2; - map<string, string> data_template = 3; - map<string, string> variables = 4; -} - -// 1. Client sends SubscribeHomeAssistantStatesRequest -// 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async) -// 3. Client sends HomeAssistantStateResponse for state changes. -message SubscribeHomeAssistantStatesRequest { - -} - -message SubscribeHomeAssistantStateResponse { - string entity_id = 1; -} - -message HomeAssistantStateResponse { - string entity_id = 1; - string state = 2; -} - -message GetTimeRequest { - -} - -message GetTimeResponse { - fixed32 epoch_seconds = 1; -} - diff --git a/esphome/api/client.py b/esphome/api/client.py index cdafcc1d74..0c52674287 100644 --- a/esphome/api/client.py +++ b/esphome/api/client.py @@ -14,7 +14,7 @@ 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, color -from esphome.py_compat import text_type, IS_PY2, byte_to_bytes, char_to_byte, format_bytes +from esphome.py_compat import text_type, IS_PY2, byte_to_bytes, char_to_byte from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -247,7 +247,7 @@ class APIClient(threading.Thread): if self._socket is None: raise APIConnectionError("Socket closed") - _LOGGER.debug("Write: %s", format_bytes(data)) + # _LOGGER.debug("Write: %s", format_bytes(data)) with self._socket_write_lock: try: self._socket.sendall(data) @@ -275,7 +275,7 @@ class APIClient(threading.Thread): req += encoded self._write(req) - def _send_message_await_response_complex(self, send_msg, do_append, do_stop, timeout=1): + def _send_message_await_response_complex(self, send_msg, do_append, do_stop, timeout=5): event = threading.Event() responses = [] @@ -296,7 +296,7 @@ class APIClient(threading.Thread): raise APIConnectionError("Timeout while waiting for message response!") return responses - def _send_message_await_response(self, send_msg, response_type, timeout=1): + def _send_message_await_response(self, send_msg, response_type, timeout=5): def is_response(msg): return isinstance(msg, response_type) @@ -321,13 +321,13 @@ class APIClient(threading.Thread): self._close_socket() if self.on_disconnect is not None and on_disconnect: - self.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=None, dump_config=False): + def subscribe_logs(self, on_log, log_level=7, dump_config=False): self._check_authenticated() def on_msg(msg): @@ -336,8 +336,7 @@ class APIClient(threading.Thread): self._message_handlers.append(on_msg) req = pb.SubscribeLogsRequest(dump_config=dump_config) - if log_level is not None: - req.level = log_level + req.level = log_level self._send_message(req) def _recv(self, amount): @@ -411,7 +410,7 @@ class APIClient(threading.Thread): self._socket = None self._connected = False if self.on_disconnect is not None: - self.on_disconnect() + self.on_disconnect(None) elif isinstance(msg, pb.PingRequest): self._send_message(pb.PingResponse()) elif isinstance(msg, pb.GetTimeRequest): @@ -453,7 +452,7 @@ def run_logs(config, address): _LOGGER.info("Successfully connected to %s", address) return - wait_time = min(2**tries, 300) + wait_time = int(min(1.5**min(tries, 100), 30)) if not has_connects: _LOGGER.warning(u"Initial connection failed. The ESP might not be connected " u"to WiFi yet (%s). Re-Trying in %s seconds", diff --git a/esphome/components/ade7953/__init__.py b/esphome/components/ade7953/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ade7953/ade7953.cpp b/esphome/components/ade7953/ade7953.cpp new file mode 100644 index 0000000000..9316d9cad0 --- /dev/null +++ b/esphome/components/ade7953/ade7953.cpp @@ -0,0 +1,51 @@ +#include "ade7953.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ade7953 { + +static const char *TAG = "ade7953"; + +void ADE7953::dump_config() { + ESP_LOGCONFIG(TAG, "ADE7953:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Voltage Sensor", this->voltage_sensor_); + LOG_SENSOR(" ", "Current A Sensor", this->current_a_sensor_); + LOG_SENSOR(" ", "Current B Sensor", this->current_b_sensor_); + LOG_SENSOR(" ", "Active Power A Sensor", this->active_power_a_sensor_); + LOG_SENSOR(" ", "Active Power B Sensor", this->active_power_b_sensor_); +} + +#define ADE_PUBLISH_(name, factor) \ + if (name) { \ + float value = *name / factor; \ + this->name##_sensor_->publish_state(value); \ + } +#define ADE_PUBLISH(name, factor) ADE_PUBLISH_(name, factor) + +void ADE7953::update() { + if (!this->is_setup_) + return; + + auto active_power_a = this->ade_read_<int32_t>(0x0312); + ADE_PUBLISH(active_power_a, 154.0f); + auto active_power_b = this->ade_read_<int32_t>(0x0313); + ADE_PUBLISH(active_power_b, 154.0f); + auto current_a = this->ade_read_<uint32_t>(0x031A); + ADE_PUBLISH(current_a, 100000.0f); + auto current_b = this->ade_read_<uint32_t>(0x031B); + ADE_PUBLISH(current_b, 100000.0f); + auto voltage = this->ade_read_<uint32_t>(0x031C); + ADE_PUBLISH(voltage, 26000.0f); + + // auto apparent_power_a = this->ade_read_<int32_t>(0x0310); + // auto apparent_power_b = this->ade_read_<int32_t>(0x0311); + // auto reactive_power_a = this->ade_read_<int32_t>(0x0314); + // auto reactive_power_b = this->ade_read_<int32_t>(0x0315); + // auto power_factor_a = this->ade_read_<int16_t>(0x010A); + // auto power_factor_b = this->ade_read_<int16_t>(0x010B); +} + +} // namespace ade7953 +} // namespace esphome diff --git a/esphome/components/ade7953/ade7953.h b/esphome/components/ade7953/ade7953.h new file mode 100644 index 0000000000..7591bc1684 --- /dev/null +++ b/esphome/components/ade7953/ade7953.h @@ -0,0 +1,67 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace ade7953 { + +class ADE7953 : public i2c::I2CDevice, public PollingComponent { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_a_sensor(sensor::Sensor *current_a_sensor) { current_a_sensor_ = current_a_sensor; } + void set_current_b_sensor(sensor::Sensor *current_b_sensor) { current_b_sensor_ = current_b_sensor; } + void set_active_power_a_sensor(sensor::Sensor *active_power_a_sensor) { + active_power_a_sensor_ = active_power_a_sensor; + } + void set_active_power_b_sensor(sensor::Sensor *active_power_b_sensor) { + active_power_b_sensor_ = active_power_b_sensor; + } + + void setup() override { + this->set_timeout(100, [this]() { + this->ade_write_<uint8_t>(0x0010, 0x04); + this->ade_write_<uint8_t>(0x00FE, 0xAD); + this->ade_write_<uint16_t>(0x0120, 0x0030); + this->is_setup_ = true; + }); + } + + void dump_config() override; + + void update() override; + + protected: + template<typename T> bool ade_write_(uint16_t reg, T value) { + std::vector<uint8_t> data; + data.push_back(reg >> 8); + data.push_back(reg >> 0); + for (int i = sizeof(T) - 1; i >= 0; i--) + data.push_back(value >> (i * 8)); + return this->write_bytes_raw(data); + } + template<typename T> optional<T> ade_read_(uint16_t reg) { + uint8_t hi = reg >> 8; + uint8_t lo = reg >> 0; + if (!this->write_bytes_raw({hi, lo})) + return {}; + auto ret = this->read_bytes_raw<sizeof(T)>(); + if (!ret.has_value()) + return {}; + T result = 0; + for (int i = 0, j = sizeof(T) - 1; i < sizeof(T); i++, j--) + result |= T((*ret)[i]) << (j * 8); + return result; + } + + bool is_setup_{false}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_a_sensor_{nullptr}; + sensor::Sensor *current_b_sensor_{nullptr}; + sensor::Sensor *active_power_a_sensor_{nullptr}; + sensor::Sensor *active_power_b_sensor_{nullptr}; +}; + +} // namespace ade7953 +} // namespace esphome diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py new file mode 100644 index 0000000000..4fcd307332 --- /dev/null +++ b/esphome/components/ade7953/sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, i2c +from esphome.const import CONF_ID, CONF_VOLTAGE, \ + UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT + +DEPENDENCIES = ['i2c'] + +ace7953_ns = cg.esphome_ns.namespace('ade7953') +ADE7953 = ace7953_ns.class_('ADE7953', cg.PollingComponent, i2c.I2CDevice) + +CONF_CURRENT_A = 'current_a' +CONF_CURRENT_B = 'current_b' +CONF_ACTIVE_POWER_A = 'active_power_a' +CONF_ACTIVE_POWER_B = 'active_power_b' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(ADE7953), + + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT_A): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_CURRENT_B): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 1), + cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 1), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x38)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + for key in [CONF_VOLTAGE, CONF_CURRENT_A, CONF_CURRENT_B, CONF_ACTIVE_POWER_A, + CONF_ACTIVE_POWER_B]: + if key not in config: + continue + conf = config[key] + sens = yield sensor.new_sensor(conf) + cg.add(getattr(var, 'set_{}_sensor'.format(key))(sens)) diff --git a/esphome/components/ads1115/__init__.py b/esphome/components/ads1115/__init__.py index 28cfac49ec..a41a521ba7 100644 --- a/esphome/components/ads1115/__init__.py +++ b/esphome/components/ads1115/__init__.py @@ -10,8 +10,10 @@ MULTI_CONF = True ads1115_ns = cg.esphome_ns.namespace('ads1115') ADS1115Component = ads1115_ns.class_('ADS1115Component', cg.Component, i2c.I2CDevice) +CONF_CONTINUOUS_MODE = 'continuous_mode' CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(ADS1115Component), + cv.Optional(CONF_CONTINUOUS_MODE, default=False): cv.boolean, }).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(None)) @@ -19,3 +21,5 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) yield i2c.register_i2c_device(var, config) + + cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE])) diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index 0d82fce533..0899571a47 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -29,9 +29,15 @@ void ADS1115Component::setup() { // 0bxxxx000xxxxxxxxx config |= ADS1115_GAIN_6P144 << 9; - // Set singleshot mode - // 0bxxxxxxx1xxxxxxxx - config |= 0b0000000100000000; + if (this->continuous_mode_) { + // Set continuous mode + // 0bxxxxxxx0xxxxxxxx + config |= 0b0000000000000000; + } else { + // Set singleshot mode + // 0bxxxxxxx1xxxxxxxx + config |= 0b0000000100000000; + } // Set data rate - 860 samples per second (we're in singleshot mode) // 0bxxxxxxxx100xxxxx @@ -57,6 +63,8 @@ void ADS1115Component::setup() { this->mark_failed(); return; } + this->prev_config_ = config; + for (auto *sensor : this->sensors_) { this->set_interval(sensor->get_name(), sensor->update_interval(), [this, sensor] { this->request_measurement(sensor); }); @@ -75,13 +83,8 @@ void ADS1115Component::dump_config() { ESP_LOGCONFIG(TAG, " Gain: %u", sensor->get_gain()); } } -float ADS1115Component::get_setup_priority() const { return setup_priority::DATA; } float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { - uint16_t config; - if (!this->read_byte_16(ADS1115_REGISTER_CONFIG, &config)) { - this->status_set_warning(); - return NAN; - } + uint16_t config = this->prev_config_; // Multiplexer // 0bxBBBxxxxxxxxxxxx config &= 0b1000111111111111; @@ -91,25 +94,31 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { // 0bxxxxBBBxxxxxxxxx config &= 0b1111000111111111; config |= (sensor->get_gain() & 0b111) << 9; - // Start conversion - config |= 0b1000000000000000; - if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) { - this->status_set_warning(); - return NAN; + if (!this->continuous_mode_) { + // Start conversion + config |= 0b1000000000000000; } - // about 1.6 ms with 860 samples per second - delay(2); - - uint32_t start = millis(); - while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) { - if (millis() - start > 100) { - ESP_LOGW(TAG, "Reading ADS1115 timed out"); + if (!this->continuous_mode_ || this->prev_config_ != config) { + if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) { this->status_set_warning(); return NAN; } - yield(); + this->prev_config_ = config; + + // about 1.6 ms with 860 samples per second + delay(2); + + uint32_t start = millis(); + while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) { + if (millis() - start > 100) { + ESP_LOGW(TAG, "Reading ADS1115 timed out"); + this->status_set_warning(); + return NAN; + } + yield(); + } } uint16_t raw_conversion; @@ -147,10 +156,6 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { return millivolts / 1e3f; } -uint8_t ADS1115Sensor::get_multiplexer() const { return this->multiplexer_; } -void ADS1115Sensor::set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; } -uint8_t ADS1115Sensor::get_gain() const { return this->gain_; } -void ADS1115Sensor::set_gain(ADS1115Gain gain) { this->gain_ = gain; } float ADS1115Sensor::sample() { return this->parent_->request_measurement(this); } void ADS1115Sensor::update() { float v = this->parent_->request_measurement(this); diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h index 966541ef4f..059436d142 100644 --- a/esphome/components/ads1115/ads1115.h +++ b/esphome/components/ads1115/ads1115.h @@ -37,13 +37,16 @@ class ADS1115Component : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; /// HARDWARE_LATE setup priority - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } /// Helper method to request a measurement from a sensor. float request_measurement(ADS1115Sensor *sensor); protected: std::vector<ADS1115Sensor *> sensors_; + uint16_t prev_config_{0}; + bool continuous_mode_; }; /// Internal holder class that is in instance of Sensor so that the hub can create individual sensors. @@ -51,12 +54,12 @@ class ADS1115Sensor : public sensor::Sensor, public PollingComponent, public vol public: ADS1115Sensor(ADS1115Component *parent) : parent_(parent) {} void update() override; - void set_multiplexer(ADS1115Multiplexer multiplexer); - void set_gain(ADS1115Gain gain); + void set_multiplexer(ADS1115Multiplexer multiplexer) { multiplexer_ = multiplexer; } + void set_gain(ADS1115Gain gain) { gain_ = gain; } float sample() override; - uint8_t get_multiplexer() const; - uint8_t get_gain() const; + uint8_t get_multiplexer() const { return multiplexer_; } + uint8_t get_gain() const { return gain_; } protected: ADS1115Component *parent_; diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 5c15c68db7..43c7d71e74 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -3,44 +3,40 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition from esphome.const import CONF_DATA, CONF_DATA_TEMPLATE, CONF_ID, CONF_PASSWORD, CONF_PORT, \ - CONF_REBOOT_TIMEOUT, CONF_SERVICE, CONF_VARIABLES, CONF_SERVICES, CONF_TRIGGER_ID -from esphome.core import CORE, coroutine_with_priority + CONF_REBOOT_TIMEOUT, CONF_SERVICE, CONF_VARIABLES, CONF_SERVICES, CONF_TRIGGER_ID, CONF_EVENT +from esphome.core import coroutine_with_priority DEPENDENCIES = ['network'] +AUTO_LOAD = ['async_tcp'] api_ns = cg.esphome_ns.namespace('api') APIServer = api_ns.class_('APIServer', cg.Component, cg.Controller) HomeAssistantServiceCallAction = api_ns.class_('HomeAssistantServiceCallAction', automation.Action) -KeyValuePair = api_ns.class_('KeyValuePair') -TemplatableKeyValuePair = api_ns.class_('TemplatableKeyValuePair') APIConnectedCondition = api_ns.class_('APIConnectedCondition', Condition) -UserService = api_ns.class_('UserService', automation.Trigger) -ServiceTypeArgument = api_ns.class_('ServiceTypeArgument') -ServiceArgType = api_ns.enum('ServiceArgType') -SERVICE_ARG_TYPES = { - 'bool': ServiceArgType.SERVICE_ARG_TYPE_BOOL, - 'int': ServiceArgType.SERVICE_ARG_TYPE_INT, - 'float': ServiceArgType.SERVICE_ARG_TYPE_FLOAT, - 'string': ServiceArgType.SERVICE_ARG_TYPE_STRING, -} +UserServiceTrigger = api_ns.class_('UserServiceTrigger', automation.Trigger) +ListEntitiesServicesArgument = api_ns.class_('ListEntitiesServicesArgument') SERVICE_ARG_NATIVE_TYPES = { 'bool': bool, 'int': cg.int32, 'float': float, 'string': cg.std_string, + 'bool[]': cg.std_vector.template(bool), + 'int[]': cg.std_vector.template(cg.int32), + 'float[]': cg.std_vector.template(float), + 'string[]': cg.std_vector.template(cg.std_string), } CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(APIServer), cv.Optional(CONF_PORT, default=6053): cv.port, cv.Optional(CONF_PASSWORD, default=''): cv.string_strict, - cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds, cv.Optional(CONF_SERVICES): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserService), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), cv.Required(CONF_SERVICE): cv.valid_name, cv.Optional(CONF_VARIABLES, default={}): cv.Schema({ - cv.validate_id_name: cv.one_of(*SERVICE_ARG_TYPES, lower=True), + cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), }), }), }).extend(cv.COMPONENT_SCHEMA) @@ -58,37 +54,30 @@ def to_code(config): for conf in config.get(CONF_SERVICES, []): template_args = [] func_args = [] - service_type_args = [] + service_arg_names = [] for name, var_ in conf[CONF_VARIABLES].items(): native = SERVICE_ARG_NATIVE_TYPES[var_] template_args.append(native) func_args.append((native, name)) - service_type_args.append(ServiceTypeArgument(name, SERVICE_ARG_TYPES[var_])) + service_arg_names.append(name) templ = cg.TemplateArguments(*template_args) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], templ, - conf[CONF_SERVICE], service_type_args) + conf[CONF_SERVICE], service_arg_names) cg.add(var.register_user_service(trigger)) yield automation.build_automation(trigger, func_args, conf) cg.add_define('USE_API') - if CORE.is_esp32: - cg.add_library('AsyncTCP', '1.0.3') - elif CORE.is_esp8266: - cg.add_library('ESPAsyncTCP', '1.2.0') + cg.add_global(api_ns.using) +KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string)}) + HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema({ cv.GenerateID(): cv.use_id(APIServer), - cv.Required(CONF_SERVICE): cv.string, - cv.Optional(CONF_DATA): cv.Schema({ - cv.string: cv.string, - }), - cv.Optional(CONF_DATA_TEMPLATE): cv.Schema({ - cv.string: cv.string, - }), - cv.Optional(CONF_VARIABLES): cv.Schema({ - cv.string: cv.returning_lambda, - }), + cv.Required(CONF_SERVICE): cv.templatable(cv.string), + cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_VARIABLES, default={}): KEY_VALUE_SCHEMA, }) @@ -96,20 +85,54 @@ HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema({ HOMEASSISTANT_SERVICE_ACTION_SCHEMA) def homeassistant_service_to_code(config, action_id, template_arg, args): serv = yield cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, serv) - cg.add(var.set_service(config[CONF_SERVICE])) - if CONF_DATA in config: - datas = [KeyValuePair(k, v) for k, v in config[CONF_DATA].items()] - cg.add(var.set_data(datas)) - if CONF_DATA_TEMPLATE in config: - datas = [KeyValuePair(k, v) for k, v in config[CONF_DATA_TEMPLATE].items()] - cg.add(var.set_data_template(datas)) - if CONF_VARIABLES in config: - datas = [] - for key, value in config[CONF_VARIABLES].items(): - value_ = yield cg.process_lambda(value, []) - datas.append(TemplatableKeyValuePair(key, value_)) - cg.add(var.set_variables(datas)) + var = cg.new_Pvariable(action_id, template_arg, serv, False) + templ = yield cg.templatable(config[CONF_SERVICE], args, None) + cg.add(var.set_service(templ)) + for key, value in config[CONF_DATA].items(): + templ = yield cg.templatable(value, args, None) + cg.add(var.add_data(key, templ)) + for key, value in config[CONF_DATA_TEMPLATE].items(): + templ = yield cg.templatable(value, args, None) + cg.add(var.add_data_template(key, templ)) + for key, value in config[CONF_VARIABLES].items(): + templ = yield cg.templatable(value, args, None) + cg.add(var.add_variable(key, templ)) + yield var + + +def validate_homeassistant_event(value): + value = cv.string(value) + if not value.startswith(u'esphome.'): + raise cv.Invalid("ESPHome can only generate Home Assistant events that begin with " + "esphome. For example 'esphome.xyz'") + return value + + +HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(APIServer), + cv.Required(CONF_EVENT): validate_homeassistant_event, + cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_VARIABLES, default={}): KEY_VALUE_SCHEMA, +}) + + +@automation.register_action('homeassistant.event', HomeAssistantServiceCallAction, + HOMEASSISTANT_EVENT_ACTION_SCHEMA) +def homeassistant_event_to_code(config, action_id, template_arg, args): + serv = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, serv, True) + templ = yield cg.templatable(config[CONF_EVENT], args, None) + cg.add(var.set_service(templ)) + for key, value in config[CONF_DATA].items(): + templ = yield cg.templatable(value, args, None) + cg.add(var.add_data(key, templ)) + for key, value in config[CONF_DATA_TEMPLATE].items(): + templ = yield cg.templatable(value, args, None) + cg.add(var.add_data_template(key, templ)) + for key, value in config[CONF_VARIABLES].items(): + templ = yield cg.templatable(value, args, None) + cg.add(var.add_variable(key, templ)) yield var diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4d6f2ac9a7..175bd3858f 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1,5 +1,45 @@ syntax = "proto3"; +import "api_options.proto"; + +service APIConnection { + rpc hello (HelloRequest) returns (HelloResponse) { + option (needs_setup_connection) = false; + option (needs_authentication) = false; + } + rpc connect (ConnectRequest) returns (ConnectResponse) { + option (needs_setup_connection) = false; + option (needs_authentication) = false; + } + rpc disconnect (DisconnectRequest) returns (DisconnectResponse) { + option (needs_setup_connection) = false; + option (needs_authentication) = false; + } + rpc ping (PingRequest) returns (PingResponse) { + option (needs_setup_connection) = false; + option (needs_authentication) = false; + } + rpc device_info (DeviceInfoRequest) returns (DeviceInfoResponse) { + option (needs_authentication) = false; + } + rpc list_entities (ListEntitiesRequest) returns (void) {} + rpc subscribe_states (SubscribeStatesRequest) returns (void) {} + rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} + rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {} + rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {} + rpc get_time (GetTimeRequest) returns (GetTimeResponse) { + option (needs_authentication) = false; + } + rpc execute_service (ExecuteServiceRequest) returns (void) {} + + rpc cover_command (CoverCommandRequest) returns (void) {} + rpc fan_command (FanCommandRequest) returns (void) {} + rpc light_command (LightCommandRequest) returns (void) {} + rpc switch_command (SwitchCommandRequest) returns (void) {} + rpc camera_image (CameraImageRequest) returns (void) {} + rpc climate_command (ClimateCommandRequest) returns (void) {} +} + // ==================== BASE PACKETS ==================== @@ -21,8 +61,11 @@ syntax = "proto3"; // Message sent at the beginning of each connection // Can only be sent by the client and only at the beginning of the connection -// ID: 1 message HelloRequest { + option (id) = 1; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + // Description of client (like User Agent) // For example "Home Assistant" // Not strictly necessary to send but nice for debugging @@ -32,8 +75,11 @@ message HelloRequest { // Confirmation of successful connection request. // Can only be sent by the server and only at the beginning of the connection -// ID: 2 message HelloResponse { + option (id) = 2; + option (source) = SOURCE_SERVER; + option (no_delay) = true; + // The version of the API to use. The _client_ (for example Home Assistant) needs to check // for compatibility and if necessary adopt to an older API. // Major is for breaking changes in the base protocol - a mismatch will lead to immediate disconnect_client_ @@ -49,49 +95,66 @@ message HelloResponse { // Message sent at the beginning of each connection to authenticate the client // Can only be sent by the client and only at the beginning of the connection -// ID: 3 message ConnectRequest { + option (id) = 3; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + // The password to log in with string password = 1; } // Confirmation of successful connection. After this the connection is available for all traffic. // Can only be sent by the server and only at the beginning of the connection -// ID: 4 message ConnectResponse { + option (id) = 4; + option (source) = SOURCE_SERVER; + option (no_delay) = true; + bool invalid_password = 1; } // Request to close the connection. // Can be sent by both the client and server -// ID: 5 message DisconnectRequest { + option (id) = 5; + option (source) = SOURCE_BOTH; + option (no_delay) = true; + // Do not close the connection before the acknowledgement arrives } -// ID: 6 message DisconnectResponse { + option (id) = 6; + option (source) = SOURCE_BOTH; + option (no_delay) = true; + // Empty - Both parties are required to close the connection after this // message has been received. } -// ID: 7 message PingRequest { + option (id) = 7; + option (source) = SOURCE_BOTH; // Empty } -// ID: 8 message PingResponse { + option (id) = 8; + option (source) = SOURCE_BOTH; // Empty } -// ID: 9 message DeviceInfoRequest { + option (id) = 9; + option (source) = SOURCE_CLIENT; // Empty } -// ID: 10 message DeviceInfoResponse { + option (id) = 10; + option (source) = SOURCE_SERVER; + bool uses_password = 1; // The name of the node, given by "App.set_name()" @@ -101,7 +164,7 @@ message DeviceInfoResponse { string mac_address = 3; // A string describing the ESPHome version. For example "1.10.0" - string esphome_core_version = 4; + string esphome_version = 4; // A string describing the date of compilation, this is generated by the compiler // and therefore may not be in the same format all the time. @@ -114,22 +177,29 @@ message DeviceInfoResponse { bool has_deep_sleep = 7; } -// ID: 11 message ListEntitiesRequest { + option (id) = 11; + option (source) = SOURCE_CLIENT; // Empty } -// ID: 19 message ListEntitiesDoneResponse { + option (id) = 19; + option (source) = SOURCE_SERVER; + option (no_delay) = true; // Empty } -// ID: 20 message SubscribeStatesRequest { + option (id) = 20; + option (source) = SOURCE_CLIENT; // Empty } // ==================== BINARY SENSOR ==================== -// ID: 12 message ListEntitiesBinarySensorResponse { + option (id) = 12; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BINARY_SENSOR"; + string object_id = 1; fixed32 key = 2; string name = 3; @@ -138,15 +208,22 @@ message ListEntitiesBinarySensorResponse { string device_class = 5; bool is_status_binary_sensor = 6; } -// ID: 21 message BinarySensorStateResponse { + option (id) = 21; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BINARY_SENSOR"; + option (no_delay) = true; + fixed32 key = 1; bool state = 2; } // ==================== COVER ==================== -// ID: 13 message ListEntitiesCoverResponse { + option (id) = 13; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_COVER"; + string object_id = 1; fixed32 key = 2; string name = 3; @@ -157,38 +234,47 @@ message ListEntitiesCoverResponse { bool supports_tilt = 7; string device_class = 8; } -// ID: 22 -message CoverStateResponse { - fixed32 key = 1; +enum LegacyCoverState { + LEGACY_COVER_STATE_OPEN = 0; + LEGACY_COVER_STATE_CLOSED = 1; +} +enum CoverOperation { + COVER_OPERATION_IDLE = 0; + COVER_OPERATION_IS_OPENING = 1; + COVER_OPERATION_IS_CLOSING = 2; +} +message CoverStateResponse { + option (id) = 22; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_COVER"; + option (no_delay) = true; + + fixed32 key = 1; // legacy: state has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change - enum LegacyCoverState { - OPEN = 0; - CLOSED = 1; - } LegacyCoverState legacy_state = 2; float position = 3; float tilt = 4; - enum CoverOperation { - IDLE = 0; - IS_OPENING = 1; - IS_CLOSING = 2; - } CoverOperation current_operation = 5; } -// ID: 30 + +enum LegacyCoverCommand { + LEGACY_COVER_COMMAND_OPEN = 0; + LEGACY_COVER_COMMAND_CLOSE = 1; + LEGACY_COVER_COMMAND_STOP = 2; +} message CoverCommandRequest { + option (id) = 30; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_COVER"; + option (no_delay) = true; + fixed32 key = 1; // legacy: command has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change - enum LegacyCoverCommand { - OPEN = 0; - CLOSE = 1; - STOP = 2; - } bool has_legacy_command = 2; LegacyCoverCommand legacy_command = 3; @@ -200,8 +286,11 @@ message CoverCommandRequest { } // ==================== FAN ==================== -// ID: 14 message ListEntitiesFanResponse { + option (id) = 14; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_FAN"; + string object_id = 1; fixed32 key = 2; string name = 3; @@ -211,19 +300,27 @@ message ListEntitiesFanResponse { bool supports_speed = 6; } enum FanSpeed { - LOW = 0; - MEDIUM = 1; - HIGH = 2; + FAN_SPEED_LOW = 0; + FAN_SPEED_MEDIUM = 1; + FAN_SPEED_HIGH = 2; } -// ID: 23 message FanStateResponse { + option (id) = 23; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_FAN"; + option (no_delay) = true; + fixed32 key = 1; bool state = 2; bool oscillating = 3; FanSpeed speed = 4; } -// ID: 31 message FanCommandRequest { + option (id) = 31; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_FAN"; + option (no_delay) = true; + fixed32 key = 1; bool has_state = 2; bool state = 3; @@ -234,8 +331,11 @@ message FanCommandRequest { } // ==================== LIGHT ==================== -// ID: 15 message ListEntitiesLightResponse { + option (id) = 15; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_LIGHT"; + string object_id = 1; fixed32 key = 2; string name = 3; @@ -249,8 +349,12 @@ message ListEntitiesLightResponse { float max_mireds = 10; repeated string effects = 11; } -// ID: 24 message LightStateResponse { + option (id) = 24; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_LIGHT"; + option (no_delay) = true; + fixed32 key = 1; bool state = 2; float brightness = 3; @@ -261,8 +365,12 @@ message LightStateResponse { float color_temperature = 8; string effect = 9; } -// ID: 32 message LightCommandRequest { + option (id) = 32; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_LIGHT"; + option (no_delay) = true; + fixed32 key = 1; bool has_state = 2; bool state = 3; @@ -285,8 +393,11 @@ message LightCommandRequest { } // ==================== SENSOR ==================== -// ID: 16 message ListEntitiesSensorResponse { + option (id) = 16; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SENSOR"; + string object_id = 1; fixed32 key = 2; string name = 3; @@ -295,16 +406,24 @@ message ListEntitiesSensorResponse { string icon = 5; string unit_of_measurement = 6; int32 accuracy_decimals = 7; + bool force_update = 8; } -// ID: 25 message SensorStateResponse { + option (id) = 25; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SENSOR"; + option (no_delay) = true; + fixed32 key = 1; float state = 2; } // ==================== SWITCH ==================== -// ID: 17 message ListEntitiesSwitchResponse { + option (id) = 17; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SWITCH"; + string object_id = 1; fixed32 key = 2; string name = 3; @@ -313,20 +432,31 @@ message ListEntitiesSwitchResponse { string icon = 5; bool assumed_state = 6; } -// ID: 26 message SwitchStateResponse { + option (id) = 26; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SWITCH"; + option (no_delay) = true; + fixed32 key = 1; bool state = 2; } -// ID: 33 message SwitchCommandRequest { + option (id) = 33; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SWITCH"; + option (no_delay) = true; + fixed32 key = 1; bool state = 2; } // ==================== TEXT SENSOR ==================== -// ID: 18 message ListEntitiesTextSensorResponse { + option (id) = 18; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_TEXT_SENSOR"; + string object_id = 1; fixed32 key = 2; string name = 3; @@ -334,29 +464,38 @@ message ListEntitiesTextSensorResponse { string icon = 5; } -// ID: 27 message TextSensorStateResponse { + option (id) = 27; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_TEXT_SENSOR"; + option (no_delay) = true; + fixed32 key = 1; string state = 2; } // ==================== SUBSCRIBE LOGS ==================== enum LogLevel { - NONE = 0; - ERROR = 1; - WARN = 2; - INFO = 3; - DEBUG = 4; - VERBOSE = 5; - VERY_VERBOSE = 6; + LOG_LEVEL_NONE = 0; + LOG_LEVEL_ERROR = 1; + LOG_LEVEL_WARN = 2; + LOG_LEVEL_INFO = 3; + LOG_LEVEL_DEBUG = 4; + LOG_LEVEL_VERBOSE = 5; + LOG_LEVEL_VERY_VERBOSE = 6; } -// ID: 28 message SubscribeLogsRequest { + option (id) = 28; + option (source) = SOURCE_CLIENT; LogLevel level = 1; bool dump_config = 2; } -// ID: 29 message SubscribeLogsResponse { + option (id) = 29; + option (source) = SOURCE_SERVER; + option (log) = false; + option (no_delay) = false; + LogLevel level = 1; string tag = 2; string message = 3; @@ -364,109 +503,159 @@ message SubscribeLogsResponse { } // ==================== HOMEASSISTANT.SERVICE ==================== -// ID: 34 -message SubscribeServiceCallsRequest { - +message SubscribeHomeassistantServicesRequest { + option (id) = 34; + option (source) = SOURCE_CLIENT; } -// ID: 35 -message ServiceCallResponse { +message HomeassistantServiceMap { + string key = 1; + string value = 2; +} + +message HomeassistantServiceResponse { + option (id) = 35; + option (source) = SOURCE_SERVER; + option (no_delay) = true; + string service = 1; - map<string, string> data = 2; - map<string, string> data_template = 3; - map<string, string> variables = 4; + repeated HomeassistantServiceMap data = 2; + repeated HomeassistantServiceMap data_template = 3; + repeated HomeassistantServiceMap variables = 4; + bool is_event = 5; } // ==================== IMPORT HOME ASSISTANT STATES ==================== // 1. Client sends SubscribeHomeAssistantStatesRequest // 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async) // 3. Client sends HomeAssistantStateResponse for state changes. -// ID: 38 message SubscribeHomeAssistantStatesRequest { - + option (id) = 38; + option (source) = SOURCE_CLIENT; } -// ID: 39 message SubscribeHomeAssistantStateResponse { + option (id) = 39; + option (source) = SOURCE_SERVER; string entity_id = 1; } -// ID: 40 message HomeAssistantStateResponse { + option (id) = 40; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + string entity_id = 1; string state = 2; } // ==================== IMPORT TIME ==================== -// ID: 36 message GetTimeRequest { - + option (id) = 36; + option (source) = SOURCE_BOTH; } -// ID: 37 message GetTimeResponse { + option (id) = 37; + option (source) = SOURCE_BOTH; + option (no_delay) = true; + fixed32 epoch_seconds = 1; } // ==================== USER-DEFINES SERVICES ==================== +enum ServiceArgType { + SERVICE_ARG_TYPE_BOOL = 0; + SERVICE_ARG_TYPE_INT = 1; + SERVICE_ARG_TYPE_FLOAT = 2; + SERVICE_ARG_TYPE_STRING = 3; + SERVICE_ARG_TYPE_BOOL_ARRAY = 4; + SERVICE_ARG_TYPE_INT_ARRAY = 5; + SERVICE_ARG_TYPE_FLOAT_ARRAY = 6; + SERVICE_ARG_TYPE_STRING_ARRAY = 7; +} message ListEntitiesServicesArgument { string name = 1; - enum Type { - BOOL = 0; - INT = 1; - FLOAT = 2; - STRING = 3; - } - Type type = 2; + ServiceArgType type = 2; } -// ID: 41 message ListEntitiesServicesResponse { + option (id) = 41; + option (source) = SOURCE_SERVER; + string name = 1; fixed32 key = 2; repeated ListEntitiesServicesArgument args = 3; } message ExecuteServiceArgument { bool bool_ = 1; - int32 int_ = 2; + int32 legacy_int = 2; float float_ = 3; string string_ = 4; + // ESPHome 1.14 (api v1.3) make int a signed value + sint32 int_ = 5; + repeated bool bool_array = 6 [packed=false]; + repeated sint32 int_array = 7 [packed=false]; + repeated float float_array = 8 [packed=false]; + repeated string string_array = 9; } -// ID: 42 message ExecuteServiceRequest { + option (id) = 42; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + fixed32 key = 1; repeated ExecuteServiceArgument args = 2; } // ==================== CAMERA ==================== -// ID: 43 message ListEntitiesCameraResponse { + option (id) = 43; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ESP32_CAMERA"; + string object_id = 1; fixed32 key = 2; string name = 3; string unique_id = 4; } -// ID: 44 message CameraImageResponse { + option (id) = 44; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ESP32_CAMERA"; + fixed32 key = 1; bytes data = 2; bool done = 3; } -// ID: 45 message CameraImageRequest { + option (id) = 45; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_ESP32_CAMERA"; + option (no_delay) = true; + bool single = 1; bool stream = 2; } // ==================== CLIMATE ==================== enum ClimateMode { - OFF = 0; - AUTO = 1; - COOL = 2; - HEAT = 3; + CLIMATE_MODE_OFF = 0; + CLIMATE_MODE_AUTO = 1; + CLIMATE_MODE_COOL = 2; + CLIMATE_MODE_HEAT = 3; +} +enum ClimateAction { + CLIMATE_ACTION_OFF = 0; + // values same as mode for readability + CLIMATE_ACTION_COOLING = 2; + CLIMATE_ACTION_HEATING = 3; } -// ID: 46 message ListEntitiesClimateResponse { + option (id) = 46; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_CLIMATE"; + string object_id = 1; fixed32 key = 2; string name = 3; @@ -479,9 +668,14 @@ message ListEntitiesClimateResponse { float visual_max_temperature = 9; float visual_temperature_step = 10; bool supports_away = 11; + bool supports_action = 12; } -// ID: 47 message ClimateStateResponse { + option (id) = 47; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_CLIMATE"; + option (no_delay) = true; + fixed32 key = 1; ClimateMode mode = 2; float current_temperature = 3; @@ -489,9 +683,14 @@ message ClimateStateResponse { float target_temperature_low = 5; float target_temperature_high = 6; bool away = 7; + ClimateAction action = 8; } -// ID: 48 message ClimateCommandRequest { + option (id) = 48; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_CLIMATE"; + option (no_delay) = true; + fixed32 key = 1; bool has_mode = 2; ClimateMode mode = 3; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp new file mode 100644 index 0000000000..4a595a3f99 --- /dev/null +++ b/esphome/components/api/api_connection.cpp @@ -0,0 +1,673 @@ +#include "api_connection.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" +#include "esphome/core/version.h" + +#ifdef USE_DEEP_SLEEP +#include "esphome/components/deep_sleep/deep_sleep_component.h" +#endif +#ifdef USE_HOMEASSISTANT_TIME +#include "esphome/components/homeassistant/time/homeassistant_time.h" +#endif + +namespace esphome { +namespace api { + +static const char *TAG = "api.connection"; + +APIConnection::APIConnection(AsyncClient *client, APIServer *parent) + : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { + this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this); + this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this); + this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); }, + this); + this->client_->onData([](void *s, AsyncClient *c, void *buf, + size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast<uint8_t *>(buf), len); }, + this); + + this->send_buffer_.reserve(64); + this->recv_buffer_.reserve(32); + this->client_info_ = this->client_->remoteIP().toString().c_str(); + 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()) { + if (this->recv_buffer_[0] != 0x00) { + ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str()); + this->on_fatal_error(); + return; + } + uint32_t i = 1; + const uint32_t size = this->recv_buffer_.size(); + uint32_t consumed; + auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); + if (!msg_size_varint.has_value()) + // not enough data there yet + return; + i += consumed; + uint32_t msg_size = msg_size_varint->as_uint32(); + + auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); + if (!msg_type_varint.has_value()) + // not enough data there yet + return; + i += consumed; + uint32_t msg_type = msg_type_varint->as_uint32(); + + if (size - i < msg_size) + // message body not fully received + return; + + uint8_t *msg = &this->recv_buffer_[i]; + this->read_message(msg_size, msg_type, msg); + if (this->remove_) + return; + // pop front + uint32_t total = i + msg_size; + this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total); + this->last_traffic_ = millis(); + } +} + +void APIConnection::disconnect_client() { + this->client_->close(); + this->remove_ = true; +} + +void APIConnection::loop() { + if (this->remove_) + return; + + if (this->next_close_) { + this->disconnect_client(); + return; + } + + if (!network_is_connected()) { + // when network is disconnected force disconnect immediately + // don't wait for timeout + this->on_fatal_error(); + return; + } + if (this->client_->disconnected()) { + // failsafe for disconnect logic + this->on_disconnect_(); + return; + } + this->parse_recv_buffer_(); + + this->list_entities_iterator_.advance(); + this->initial_state_iterator_.advance(); + + const uint32_t keepalive = 60000; + if (this->sent_ping_) { + // Disconnect if not responded within 2.5*keepalive + if (millis() - this->last_traffic_ > (keepalive * 5) / 2) { + ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); + this->disconnect_client(); + } + } else if (millis() - this->last_traffic_ > keepalive) { + this->sent_ping_ = true; + this->send_ping_request(PingRequest()); + } + +#ifdef USE_ESP32_CAMERA + if (this->image_reader_.available()) { + uint32_t space = this->client_->space(); + // reserve 15 bytes for metadata, and at least 64 bytes of data + if (space >= 15 + 64) { + uint32_t to_send = std::min(space - 15, this->image_reader_.available()); + auto buffer = this->create_buffer(); + // fixed32 key = 1; + buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); + // bytes data = 2; + buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); + // bool done = 3; + bool done = this->image_reader_.available() == to_send; + buffer.encode_bool(3, done); + this->set_nodelay(false); + bool success = this->send_buffer(buffer, 44); + + if (success) { + this->image_reader_.consume_data(to_send); + } + if (success && done) { + this->image_reader_.return_image(); + } + } + } +#endif +} + +std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) { + return App.get_name() + component_type + nameable->get_object_id(); +} + +#ifdef USE_BINARY_SENSOR +bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) { + if (!this->state_subscription_) + return false; + + BinarySensorStateResponse resp; + resp.key = binary_sensor->get_object_id_hash(); + resp.state = state; + return this->send_binary_sensor_state_response(resp); +} +bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) { + ListEntitiesBinarySensorResponse msg; + msg.object_id = binary_sensor->get_object_id(); + msg.key = binary_sensor->get_object_id_hash(); + msg.name = binary_sensor->get_name(); + msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor); + msg.device_class = binary_sensor->get_device_class(); + msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); + return this->send_list_entities_binary_sensor_response(msg); +} +#endif + +#ifdef USE_COVER +bool APIConnection::send_cover_state(cover::Cover *cover) { + if (!this->state_subscription_) + return false; + + auto traits = cover->get_traits(); + CoverStateResponse resp{}; + resp.key = cover->get_object_id_hash(); + resp.legacy_state = (cover->position == cover::COVER_OPEN) ? LEGACY_COVER_STATE_OPEN : LEGACY_COVER_STATE_CLOSED; + resp.position = cover->position; + if (traits.get_supports_tilt()) + resp.tilt = cover->tilt; + resp.current_operation = static_cast<EnumCoverOperation>(cover->current_operation); + return this->send_cover_state_response(resp); +} +bool APIConnection::send_cover_info(cover::Cover *cover) { + auto traits = cover->get_traits(); + ListEntitiesCoverResponse msg; + msg.key = cover->get_object_id_hash(); + msg.object_id = cover->get_object_id(); + msg.name = cover->get_name(); + msg.unique_id = get_default_unique_id("cover", cover); + msg.assumed_state = traits.get_is_assumed_state(); + msg.supports_position = traits.get_supports_position(); + msg.supports_tilt = traits.get_supports_tilt(); + msg.device_class = cover->get_device_class(); + return this->send_list_entities_cover_response(msg); +} +void APIConnection::cover_command(const CoverCommandRequest &msg) { + cover::Cover *cover = App.get_cover_by_key(msg.key); + if (cover == nullptr) + return; + + auto call = cover->make_call(); + if (msg.has_legacy_command) { + switch (msg.legacy_command) { + case LEGACY_COVER_COMMAND_OPEN: + call.set_command_open(); + break; + case LEGACY_COVER_COMMAND_CLOSE: + call.set_command_close(); + break; + case LEGACY_COVER_COMMAND_STOP: + call.set_command_stop(); + break; + } + } + if (msg.has_position) + call.set_position(msg.position); + if (msg.has_tilt) + call.set_tilt(msg.tilt); + if (msg.stop) + call.set_command_stop(); + call.perform(); +} +#endif + +#ifdef USE_FAN +bool APIConnection::send_fan_state(fan::FanState *fan) { + if (!this->state_subscription_) + return false; + + auto traits = fan->get_traits(); + FanStateResponse resp{}; + resp.key = fan->get_object_id_hash(); + resp.state = fan->state; + if (traits.supports_oscillation()) + resp.oscillating = fan->oscillating; + if (traits.supports_speed()) + resp.speed = static_cast<EnumFanSpeed>(fan->speed); + return this->send_fan_state_response(resp); +} +bool APIConnection::send_fan_info(fan::FanState *fan) { + auto traits = fan->get_traits(); + ListEntitiesFanResponse msg; + msg.key = fan->get_object_id_hash(); + msg.object_id = fan->get_object_id(); + msg.name = fan->get_name(); + msg.unique_id = get_default_unique_id("fan", fan); + msg.supports_oscillation = traits.supports_oscillation(); + msg.supports_speed = traits.supports_speed(); + return this->send_list_entities_fan_response(msg); +} +void APIConnection::fan_command(const FanCommandRequest &msg) { + fan::FanState *fan = App.get_fan_by_key(msg.key); + if (fan == nullptr) + return; + + auto call = fan->make_call(); + if (msg.has_state) + call.set_state(msg.state); + if (msg.has_oscillating) + call.set_oscillating(msg.oscillating); + if (msg.has_speed) + call.set_speed(static_cast<fan::FanSpeed>(msg.speed)); + call.perform(); +} +#endif + +#ifdef USE_LIGHT +bool APIConnection::send_light_state(light::LightState *light) { + if (!this->state_subscription_) + return false; + + auto traits = light->get_traits(); + auto values = light->remote_values; + LightStateResponse resp{}; + + resp.key = light->get_object_id_hash(); + resp.state = values.is_on(); + if (traits.get_supports_brightness()) + resp.brightness = values.get_brightness(); + if (traits.get_supports_rgb()) { + resp.red = values.get_red(); + resp.green = values.get_green(); + resp.blue = values.get_blue(); + } + if (traits.get_supports_rgb_white_value()) + resp.white = values.get_white(); + if (traits.get_supports_color_temperature()) + resp.color_temperature = values.get_color_temperature(); + if (light->supports_effects()) + resp.effect = light->get_effect_name(); + return this->send_light_state_response(resp); +} +bool APIConnection::send_light_info(light::LightState *light) { + auto traits = light->get_traits(); + ListEntitiesLightResponse msg; + msg.key = light->get_object_id_hash(); + msg.object_id = light->get_object_id(); + msg.name = light->get_name(); + msg.unique_id = get_default_unique_id("light", light); + msg.supports_brightness = traits.get_supports_brightness(); + msg.supports_rgb = traits.get_supports_rgb(); + msg.supports_white_value = traits.get_supports_rgb_white_value(); + msg.supports_color_temperature = traits.get_supports_color_temperature(); + if (msg.supports_color_temperature) { + msg.min_mireds = traits.get_min_mireds(); + msg.max_mireds = traits.get_max_mireds(); + } + if (light->supports_effects()) { + msg.effects.emplace_back("None"); + for (auto *effect : light->get_effects()) + msg.effects.push_back(effect->get_name()); + } + return this->send_list_entities_light_response(msg); +} +void APIConnection::light_command(const LightCommandRequest &msg) { + light::LightState *light = App.get_light_by_key(msg.key); + if (light == nullptr) + return; + + auto call = light->make_call(); + if (msg.has_state) + call.set_state(msg.state); + if (msg.has_brightness) + call.set_brightness(msg.brightness); + if (msg.has_rgb) { + call.set_red(msg.red); + call.set_green(msg.green); + call.set_blue(msg.blue); + } + if (msg.has_white) + call.set_white(msg.white); + if (msg.has_color_temperature) + call.set_color_temperature(msg.color_temperature); + if (msg.has_transition_length) + call.set_transition_length(msg.transition_length); + if (msg.has_flash_length) + call.set_flash_length(msg.flash_length); + if (msg.has_effect) + call.set_effect(msg.effect); + call.perform(); +} +#endif + +#ifdef USE_SENSOR +bool APIConnection::send_sensor_state(sensor::Sensor *sensor, float state) { + if (!this->state_subscription_) + return false; + + SensorStateResponse resp{}; + resp.key = sensor->get_object_id_hash(); + resp.state = state; + return this->send_sensor_state_response(resp); +} +bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { + ListEntitiesSensorResponse msg; + msg.key = sensor->get_object_id_hash(); + msg.object_id = sensor->get_object_id(); + msg.name = sensor->get_name(); + msg.unique_id = sensor->unique_id(); + if (msg.unique_id.empty()) + msg.unique_id = get_default_unique_id("sensor", sensor); + msg.icon = sensor->get_icon(); + msg.unit_of_measurement = sensor->get_unit_of_measurement(); + msg.accuracy_decimals = sensor->get_accuracy_decimals(); + return this->send_list_entities_sensor_response(msg); +} +#endif + +#ifdef USE_SWITCH +bool APIConnection::send_switch_state(switch_::Switch *a_switch, bool state) { + if (!this->state_subscription_) + return false; + + SwitchStateResponse resp{}; + resp.key = a_switch->get_object_id_hash(); + resp.state = state; + return this->send_switch_state_response(resp); +} +bool APIConnection::send_switch_info(switch_::Switch *a_switch) { + ListEntitiesSwitchResponse msg; + msg.key = a_switch->get_object_id_hash(); + msg.object_id = a_switch->get_object_id(); + msg.name = a_switch->get_name(); + msg.unique_id = get_default_unique_id("switch", a_switch); + msg.icon = a_switch->get_icon(); + msg.assumed_state = a_switch->assumed_state(); + return this->send_list_entities_switch_response(msg); +} +void APIConnection::switch_command(const SwitchCommandRequest &msg) { + switch_::Switch *a_switch = App.get_switch_by_key(msg.key); + if (a_switch == nullptr) + return; + + if (msg.state) + a_switch->turn_on(); + else + a_switch->turn_off(); +} +#endif + +#ifdef USE_TEXT_SENSOR +bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state) { + if (!this->state_subscription_) + return false; + + TextSensorStateResponse resp{}; + resp.key = text_sensor->get_object_id_hash(); + resp.state = std::move(state); + return this->send_text_sensor_state_response(resp); +} +bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) { + ListEntitiesTextSensorResponse msg; + msg.key = text_sensor->get_object_id_hash(); + msg.object_id = text_sensor->get_object_id(); + msg.name = text_sensor->get_name(); + msg.unique_id = text_sensor->unique_id(); + if (msg.unique_id.empty()) + msg.unique_id = get_default_unique_id("text_sensor", text_sensor); + msg.icon = text_sensor->get_icon(); + return this->send_list_entities_text_sensor_response(msg); +} +#endif + +#ifdef USE_CLIMATE +bool APIConnection::send_climate_state(climate::Climate *climate) { + if (!this->state_subscription_) + return false; + + auto traits = climate->get_traits(); + ClimateStateResponse resp{}; + resp.key = climate->get_object_id_hash(); + resp.mode = static_cast<EnumClimateMode>(climate->mode); + resp.action = static_cast<EnumClimateAction>(climate->action); + if (traits.get_supports_current_temperature()) + resp.current_temperature = climate->current_temperature; + if (traits.get_supports_two_point_target_temperature()) { + resp.target_temperature_low = climate->target_temperature_low; + resp.target_temperature_high = climate->target_temperature_high; + } else { + resp.target_temperature = climate->target_temperature; + } + if (traits.get_supports_away()) + resp.away = climate->away; + return this->send_climate_state_response(resp); +} +bool APIConnection::send_climate_info(climate::Climate *climate) { + auto traits = climate->get_traits(); + ListEntitiesClimateResponse msg; + msg.key = climate->get_object_id_hash(); + msg.object_id = climate->get_object_id(); + msg.name = climate->get_name(); + msg.unique_id = get_default_unique_id("climate", climate); + msg.supports_current_temperature = traits.get_supports_current_temperature(); + msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); + for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_HEAT}) { + if (traits.supports_mode(mode)) + msg.supported_modes.push_back(static_cast<EnumClimateMode>(mode)); + } + msg.visual_min_temperature = traits.get_visual_min_temperature(); + msg.visual_max_temperature = traits.get_visual_max_temperature(); + msg.visual_temperature_step = traits.get_visual_temperature_step(); + msg.supports_away = traits.get_supports_away(); + msg.supports_action = traits.get_supports_action(); + return this->send_list_entities_climate_response(msg); +} +void APIConnection::climate_command(const ClimateCommandRequest &msg) { + climate::Climate *climate = App.get_climate_by_key(msg.key); + if (climate == nullptr) + return; + + auto call = climate->make_call(); + if (msg.has_mode) + call.set_mode(static_cast<climate::ClimateMode>(msg.mode)); + if (msg.has_target_temperature) + call.set_target_temperature(msg.target_temperature); + if (msg.has_target_temperature_low) + call.set_target_temperature_low(msg.target_temperature_low); + if (msg.has_target_temperature_high) + call.set_target_temperature_high(msg.target_temperature_high); + if (msg.has_away) + call.set_away(msg.away); + call.perform(); +} +#endif + +#ifdef USE_ESP32_CAMERA +void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) { + if (!this->state_subscription_) + return; + if (this->image_reader_.available()) + return; + this->image_reader_.set_image(image); +} +bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { + ListEntitiesCameraResponse msg; + msg.key = camera->get_object_id_hash(); + msg.object_id = camera->get_object_id(); + msg.name = camera->get_name(); + msg.unique_id = get_default_unique_id("camera", camera); + return this->send_list_entities_camera_response(msg); +} +void APIConnection::camera_image(const CameraImageRequest &msg) { + if (esp32_camera::global_esp32_camera == nullptr) + return; + + if (msg.single) + esp32_camera::global_esp32_camera->request_image(); + if (msg.stream) + esp32_camera::global_esp32_camera->request_stream(); +} +#endif + +#ifdef USE_HOMEASSISTANT_TIME +void APIConnection::on_get_time_response(const GetTimeResponse &value) { + if (homeassistant::global_homeassistant_time != nullptr) + homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); +} +#endif + +bool APIConnection::send_log_message(int level, const char *tag, const char *line) { + if (this->log_subscription_ < level) + return false; + + this->set_nodelay(false); + + // Send raw so that we don't copy too much + auto buffer = this->create_buffer(); + // LogLevel level = 1; + buffer.encode_uint32(1, static_cast<uint32_t>(level)); + // string tag = 2; + // buffer.encode_string(2, tag, strlen(tag)); + // string message = 3; + buffer.encode_string(3, line, strlen(line)); + // SubscribeLogsResponse - 29 + bool success = this->send_buffer(buffer, 29); + if (!success) { + buffer = this->create_buffer(); + // bool send_failed = 4; + buffer.encode_bool(4, true); + return this->send_buffer(buffer, 29); + } else { + return true; + } +} + +HelloResponse APIConnection::hello(const HelloRequest &msg) { + this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str(); + this->client_info_ += ")"; + ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str()); + + HelloResponse resp; + resp.api_version_major = 1; + resp.api_version_minor = 3; + resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; + this->connection_state_ = ConnectionState::CONNECTED; + return resp; +} +ConnectResponse APIConnection::connect(const ConnectRequest &msg) { + bool correct = this->parent_->check_password(msg.password); + + ConnectResponse resp; + // bool invalid_password = 1; + resp.invalid_password = !correct; + if (correct) { + ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str()); + this->connection_state_ = ConnectionState::AUTHENTICATED; + +#ifdef USE_HOMEASSISTANT_TIME + if (homeassistant::global_homeassistant_time != nullptr) { + this->send_time_request(); + } +#endif + } + return resp; +} +DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { + DeviceInfoResponse resp{}; + resp.uses_password = this->parent_->uses_password(); + resp.name = App.get_name(); + resp.mac_address = get_mac_address_pretty(); + resp.esphome_version = ESPHOME_VERSION; + resp.compilation_time = App.get_compilation_time(); +#ifdef ARDUINO_BOARD + resp.model = ARDUINO_BOARD; +#endif +#ifdef USE_DEEP_SLEEP + resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; +#endif + return resp; +} +void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { + for (auto &it : this->parent_->get_state_subs()) + if (it.entity_id == msg.entity_id) + it.callback(msg.state); +} +void APIConnection::execute_service(const ExecuteServiceRequest &msg) { + bool found = false; + for (auto *service : this->parent_->get_user_services()) { + if (service->execute_service(msg)) { + found = true; + } + } + if (!found) { + ESP_LOGV(TAG, "Could not find matching service!"); + } +} +void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { + for (auto &it : this->parent_->get_state_subs()) { + SubscribeHomeAssistantStateResponse resp; + resp.entity_id = it.entity_id; + if (!this->send_subscribe_home_assistant_state_response(resp)) { + this->on_fatal_error(); + return; + } + } +} +bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { + if (this->remove_) + return false; + + std::vector<uint8_t> header; + header.push_back(0x00); + ProtoVarInt(buffer.get_buffer()->size()).encode(header); + ProtoVarInt(message_type).encode(header); + + size_t needed_space = buffer.get_buffer()->size() + header.size(); + + if (needed_space > this->client_->space()) { + delay(0); + if (needed_space > this->client_->space()) { + // SubscribeLogsResponse + if (message_type != 29) { + ESP_LOGV(TAG, "Cannot send message because of TCP buffer space"); + } + delay(0); + return false; + } + } + + this->client_->add(reinterpret_cast<char *>(header.data()), header.size()); + this->client_->add(reinterpret_cast<char *>(buffer.get_buffer()->data()), buffer.get_buffer()->size()); + bool ret = this->client_->send(); + return ret; +} +void APIConnection::on_unauthenticated_access() { + ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str()); + this->on_fatal_error(); +} +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(); +} +void APIConnection::on_fatal_error() { + ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str()); + this->client_->close(); + this->remove_ = true; +} + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h new file mode 100644 index 0000000000..f9c7ffa28d --- /dev/null +++ b/esphome/components/api/api_connection.h @@ -0,0 +1,178 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/application.h" +#include "api_pb2.h" +#include "api_pb2_service.h" +#include "api_server.h" + +namespace esphome { +namespace api { + +class APIConnection : public APIServerConnection { + public: + APIConnection(AsyncClient *client, APIServer *parent); + virtual ~APIConnection(); + + void disconnect_client(); + void loop(); + + bool send_list_info_done() { + ListEntitiesDoneResponse resp; + return this->send_list_entities_done_response(resp); + } +#ifdef USE_BINARY_SENSOR + bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state); + bool send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); +#endif +#ifdef USE_COVER + bool send_cover_state(cover::Cover *cover); + bool send_cover_info(cover::Cover *cover); + void cover_command(const CoverCommandRequest &msg) override; +#endif +#ifdef USE_FAN + bool send_fan_state(fan::FanState *fan); + bool send_fan_info(fan::FanState *fan); + void fan_command(const FanCommandRequest &msg) override; +#endif +#ifdef USE_LIGHT + bool send_light_state(light::LightState *light); + bool send_light_info(light::LightState *light); + void light_command(const LightCommandRequest &msg) override; +#endif +#ifdef USE_SENSOR + bool send_sensor_state(sensor::Sensor *sensor, float state); + bool send_sensor_info(sensor::Sensor *sensor); +#endif +#ifdef USE_SWITCH + bool send_switch_state(switch_::Switch *a_switch, bool state); + bool send_switch_info(switch_::Switch *a_switch); + void switch_command(const SwitchCommandRequest &msg) override; +#endif +#ifdef USE_TEXT_SENSOR + bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state); + bool send_text_sensor_info(text_sensor::TextSensor *text_sensor); +#endif +#ifdef USE_ESP32_CAMERA + void send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image); + bool send_camera_info(esp32_camera::ESP32Camera *camera); + void camera_image(const CameraImageRequest &msg) override; +#endif +#ifdef USE_CLIMATE + bool send_climate_state(climate::Climate *climate); + bool send_climate_info(climate::Climate *climate); + void climate_command(const ClimateCommandRequest &msg) override; +#endif + bool send_log_message(int level, const char *tag, const char *line); + void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { + if (!this->service_call_subscription_) + return; + this->send_homeassistant_service_response(call); + } +#ifdef USE_HOMEASSISTANT_TIME + void send_time_request() { + GetTimeRequest req; + this->send_get_time_request(req); + } +#endif + + void on_disconnect_response(const DisconnectResponse &value) override { + // we initiated disconnect_client + this->next_close_ = true; + } + void on_ping_response(const PingResponse &value) override { + // we initiated ping + this->sent_ping_ = false; + } + void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; +#ifdef USE_HOMEASSISTANT_TIME + void on_get_time_response(const GetTimeResponse &value) override; +#endif + HelloResponse hello(const HelloRequest &msg) override; + ConnectResponse connect(const ConnectRequest &msg) override; + DisconnectResponse disconnect(const DisconnectRequest &msg) override { + // remote initiated disconnect_client + this->next_close_ = true; + DisconnectResponse resp; + return resp; + } + PingResponse ping(const PingRequest &msg) override { return {}; } + DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; + void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } + void subscribe_states(const SubscribeStatesRequest &msg) override { + this->state_subscription_ = true; + this->initial_state_iterator_.begin(); + } + void subscribe_logs(const SubscribeLogsRequest &msg) override { + this->log_subscription_ = msg.level; + if (msg.dump_config) + App.schedule_dump_config(); + } + void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { + this->service_call_subscription_ = true; + } + void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; + GetTimeResponse get_time(const GetTimeRequest &msg) override { + // TODO + return {}; + } + void execute_service(const ExecuteServiceRequest &msg) override; + bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; } + bool is_connection_setup() override { + return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated(); + } + void on_fatal_error() override; + void on_unauthenticated_access() override; + void on_no_setup_connection() override; + ProtoWriteBuffer create_buffer() override { + this->send_buffer_.clear(); + return {&this->send_buffer_}; + } + bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; + + protected: + friend APIServer; + + void on_error_(int8_t error); + void on_disconnect_(); + void on_timeout_(uint32_t time); + void on_data_(uint8_t *buf, size_t len); + void parse_recv_buffer_(); + void set_nodelay(bool nodelay) override { + if (nodelay == this->current_nodelay_) + return; + this->client_->setNoDelay(nodelay); + this->current_nodelay_ = nodelay; + } + + enum class ConnectionState { + WAITING_FOR_HELLO, + CONNECTED, + AUTHENTICATED, + } connection_state_{ConnectionState::WAITING_FOR_HELLO}; + + bool remove_{false}; + + std::vector<uint8_t> send_buffer_; + std::vector<uint8_t> recv_buffer_; + + std::string client_info_; +#ifdef USE_ESP32_CAMERA + esp32_camera::CameraImageReader image_reader_; +#endif + + bool state_subscription_{false}; + int log_subscription_{ESPHOME_LOG_LEVEL_NONE}; + uint32_t last_traffic_; + bool sent_ping_{false}; + bool service_call_subscription_{false}; + bool current_nodelay_{false}; + bool next_close_{false}; + AsyncClient *client_; + APIServer *parent_; + InitialStateIterator initial_state_iterator_; + ListEntitiesIterator list_entities_iterator_; +}; + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_message.h b/esphome/components/api/api_message.h deleted file mode 100644 index 26c24073df..0000000000 --- a/esphome/components/api/api_message.h +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "util.h" - -namespace esphome { -namespace api { - -enum class APIMessageType { - HELLO_REQUEST = 1, - HELLO_RESPONSE = 2, - CONNECT_REQUEST = 3, - CONNECT_RESPONSE = 4, - DISCONNECT_REQUEST = 5, - DISCONNECT_RESPONSE = 6, - PING_REQUEST = 7, - PING_RESPONSE = 8, - DEVICE_INFO_REQUEST = 9, - DEVICE_INFO_RESPONSE = 10, - - LIST_ENTITIES_REQUEST = 11, - LIST_ENTITIES_BINARY_SENSOR_RESPONSE = 12, - LIST_ENTITIES_COVER_RESPONSE = 13, - LIST_ENTITIES_FAN_RESPONSE = 14, - LIST_ENTITIES_LIGHT_RESPONSE = 15, - LIST_ENTITIES_SENSOR_RESPONSE = 16, - LIST_ENTITIES_SWITCH_RESPONSE = 17, - LIST_ENTITIES_TEXT_SENSOR_RESPONSE = 18, - LIST_ENTITIES_SERVICE_RESPONSE = 41, - LIST_ENTITIES_CAMERA_RESPONSE = 43, - LIST_ENTITIES_CLIMATE_RESPONSE = 46, - LIST_ENTITIES_DONE_RESPONSE = 19, - - SUBSCRIBE_STATES_REQUEST = 20, - BINARY_SENSOR_STATE_RESPONSE = 21, - COVER_STATE_RESPONSE = 22, - FAN_STATE_RESPONSE = 23, - LIGHT_STATE_RESPONSE = 24, - SENSOR_STATE_RESPONSE = 25, - SWITCH_STATE_RESPONSE = 26, - TEXT_SENSOR_STATE_RESPONSE = 27, - CAMERA_IMAGE_RESPONSE = 44, - CLIMATE_STATE_RESPONSE = 47, - - SUBSCRIBE_LOGS_REQUEST = 28, - SUBSCRIBE_LOGS_RESPONSE = 29, - - COVER_COMMAND_REQUEST = 30, - FAN_COMMAND_REQUEST = 31, - LIGHT_COMMAND_REQUEST = 32, - SWITCH_COMMAND_REQUEST = 33, - CAMERA_IMAGE_REQUEST = 45, - CLIMATE_COMMAND_REQUEST = 48, - - SUBSCRIBE_SERVICE_CALLS_REQUEST = 34, - SERVICE_CALL_RESPONSE = 35, - GET_TIME_REQUEST = 36, - GET_TIME_RESPONSE = 37, - - SUBSCRIBE_HOME_ASSISTANT_STATES_REQUEST = 38, - SUBSCRIBE_HOME_ASSISTANT_STATE_RESPONSE = 39, - HOME_ASSISTANT_STATE_RESPONSE = 40, - - EXECUTE_SERVICE_REQUEST = 42, -}; - -class APIMessage { - public: - void decode(const uint8_t *buffer, size_t length); - virtual bool decode_varint(uint32_t field_id, uint32_t value); - virtual bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len); - virtual bool decode_32bit(uint32_t field_id, uint32_t value); - virtual APIMessageType message_type() const = 0; - - virtual void encode(APIBuffer &buffer); -}; - -} // namespace api -} // namespace esphome diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto new file mode 100644 index 0000000000..feaf39ba15 --- /dev/null +++ b/esphome/components/api/api_options.proto @@ -0,0 +1,24 @@ +syntax = "proto2"; +import "google/protobuf/descriptor.proto"; + + +enum APISourceType { + SOURCE_BOTH = 0; + SOURCE_SERVER = 1; + SOURCE_CLIENT = 2; +} + +message void {} + +extend google.protobuf.MethodOptions { + optional bool needs_setup_connection = 1038 [default=true]; + optional bool needs_authentication = 1039 [default=true]; +} + +extend google.protobuf.MessageOptions { + optional uint32 id = 1036 [default=0]; + optional APISourceType source = 1037 [default=SOURCE_BOTH]; + optional string ifdef = 1038; + optional bool log = 1039 [default=true]; + optional bool no_delay = 1040 [default=false]; +} diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp new file mode 100644 index 0000000000..3f635d1cdb --- /dev/null +++ b/esphome/components/api/api_pb2.cpp @@ -0,0 +1,2758 @@ +#include "api_pb2.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace api { + +template<> const char *proto_enum_to_string<EnumLegacyCoverState>(EnumLegacyCoverState value) { + switch (value) { + case LEGACY_COVER_STATE_OPEN: + return "LEGACY_COVER_STATE_OPEN"; + case LEGACY_COVER_STATE_CLOSED: + return "LEGACY_COVER_STATE_CLOSED"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string<EnumCoverOperation>(EnumCoverOperation value) { + switch (value) { + case COVER_OPERATION_IDLE: + return "COVER_OPERATION_IDLE"; + case COVER_OPERATION_IS_OPENING: + return "COVER_OPERATION_IS_OPENING"; + case COVER_OPERATION_IS_CLOSING: + return "COVER_OPERATION_IS_CLOSING"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string<EnumLegacyCoverCommand>(EnumLegacyCoverCommand value) { + switch (value) { + case LEGACY_COVER_COMMAND_OPEN: + return "LEGACY_COVER_COMMAND_OPEN"; + case LEGACY_COVER_COMMAND_CLOSE: + return "LEGACY_COVER_COMMAND_CLOSE"; + case LEGACY_COVER_COMMAND_STOP: + return "LEGACY_COVER_COMMAND_STOP"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string<EnumFanSpeed>(EnumFanSpeed value) { + switch (value) { + case FAN_SPEED_LOW: + return "FAN_SPEED_LOW"; + case FAN_SPEED_MEDIUM: + return "FAN_SPEED_MEDIUM"; + case FAN_SPEED_HIGH: + return "FAN_SPEED_HIGH"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string<EnumLogLevel>(EnumLogLevel value) { + switch (value) { + case LOG_LEVEL_NONE: + return "LOG_LEVEL_NONE"; + case LOG_LEVEL_ERROR: + return "LOG_LEVEL_ERROR"; + case LOG_LEVEL_WARN: + return "LOG_LEVEL_WARN"; + case LOG_LEVEL_INFO: + return "LOG_LEVEL_INFO"; + case LOG_LEVEL_DEBUG: + return "LOG_LEVEL_DEBUG"; + case LOG_LEVEL_VERBOSE: + return "LOG_LEVEL_VERBOSE"; + case LOG_LEVEL_VERY_VERBOSE: + return "LOG_LEVEL_VERY_VERBOSE"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string<EnumServiceArgType>(EnumServiceArgType value) { + switch (value) { + case SERVICE_ARG_TYPE_BOOL: + return "SERVICE_ARG_TYPE_BOOL"; + case SERVICE_ARG_TYPE_INT: + return "SERVICE_ARG_TYPE_INT"; + case SERVICE_ARG_TYPE_FLOAT: + return "SERVICE_ARG_TYPE_FLOAT"; + case SERVICE_ARG_TYPE_STRING: + return "SERVICE_ARG_TYPE_STRING"; + case SERVICE_ARG_TYPE_BOOL_ARRAY: + return "SERVICE_ARG_TYPE_BOOL_ARRAY"; + case SERVICE_ARG_TYPE_INT_ARRAY: + return "SERVICE_ARG_TYPE_INT_ARRAY"; + case SERVICE_ARG_TYPE_FLOAT_ARRAY: + return "SERVICE_ARG_TYPE_FLOAT_ARRAY"; + case SERVICE_ARG_TYPE_STRING_ARRAY: + return "SERVICE_ARG_TYPE_STRING_ARRAY"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string<EnumClimateMode>(EnumClimateMode value) { + switch (value) { + case CLIMATE_MODE_OFF: + return "CLIMATE_MODE_OFF"; + case CLIMATE_MODE_AUTO: + return "CLIMATE_MODE_AUTO"; + case CLIMATE_MODE_COOL: + return "CLIMATE_MODE_COOL"; + case CLIMATE_MODE_HEAT: + return "CLIMATE_MODE_HEAT"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string<EnumClimateAction>(EnumClimateAction value) { + switch (value) { + case CLIMATE_ACTION_OFF: + return "CLIMATE_ACTION_OFF"; + case CLIMATE_ACTION_COOLING: + return "CLIMATE_ACTION_COOLING"; + case CLIMATE_ACTION_HEATING: + return "CLIMATE_ACTION_HEATING"; + default: + return "UNKNOWN"; + } +} +bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->client_info = value.as_string(); + return true; + } + default: + return false; + } +} +void HelloRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->client_info); } +void HelloRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("HelloRequest {\n"); + out.append(" client_info: "); + out.append("'").append(this->client_info).append("'"); + out.append("\n"); + out.append("}"); +} +bool HelloResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->api_version_major = value.as_uint32(); + return true; + } + case 2: { + this->api_version_minor = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool HelloResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: { + this->server_info = value.as_string(); + return true; + } + default: + return false; + } +} +void HelloResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->api_version_major); + buffer.encode_uint32(2, this->api_version_minor); + buffer.encode_string(3, this->server_info); +} +void HelloResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("HelloResponse {\n"); + out.append(" api_version_major: "); + sprintf(buffer, "%u", this->api_version_major); + out.append(buffer); + out.append("\n"); + + out.append(" api_version_minor: "); + sprintf(buffer, "%u", this->api_version_minor); + out.append(buffer); + out.append("\n"); + + out.append(" server_info: "); + out.append("'").append(this->server_info).append("'"); + out.append("\n"); + out.append("}"); +} +bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->password = value.as_string(); + return true; + } + default: + return false; + } +} +void ConnectRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->password); } +void ConnectRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("ConnectRequest {\n"); + out.append(" password: "); + out.append("'").append(this->password).append("'"); + out.append("\n"); + out.append("}"); +} +bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->invalid_password = value.as_bool(); + return true; + } + default: + return false; + } +} +void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } +void ConnectResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ConnectResponse {\n"); + out.append(" invalid_password: "); + out.append(YESNO(this->invalid_password)); + out.append("\n"); + out.append("}"); +} +void DisconnectRequest::encode(ProtoWriteBuffer buffer) const {} +void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } +void DisconnectResponse::encode(ProtoWriteBuffer buffer) const {} +void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } +void PingRequest::encode(ProtoWriteBuffer buffer) const {} +void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } +void PingResponse::encode(ProtoWriteBuffer buffer) const {} +void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); } +void DeviceInfoRequest::encode(ProtoWriteBuffer buffer) const {} +void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } +bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->uses_password = value.as_bool(); + return true; + } + case 7: { + this->has_deep_sleep = value.as_bool(); + return true; + } + default: + return false; + } +} +bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->name = value.as_string(); + return true; + } + case 3: { + this->mac_address = value.as_string(); + return true; + } + case 4: { + this->esphome_version = value.as_string(); + return true; + } + case 5: { + this->compilation_time = value.as_string(); + return true; + } + case 6: { + this->model = value.as_string(); + return true; + } + default: + return false; + } +} +void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_bool(1, this->uses_password); + buffer.encode_string(2, this->name); + buffer.encode_string(3, this->mac_address); + buffer.encode_string(4, this->esphome_version); + buffer.encode_string(5, this->compilation_time); + buffer.encode_string(6, this->model); + buffer.encode_bool(7, this->has_deep_sleep); +} +void DeviceInfoResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("DeviceInfoResponse {\n"); + out.append(" uses_password: "); + out.append(YESNO(this->uses_password)); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" mac_address: "); + out.append("'").append(this->mac_address).append("'"); + out.append("\n"); + + out.append(" esphome_version: "); + out.append("'").append(this->esphome_version).append("'"); + out.append("\n"); + + out.append(" compilation_time: "); + out.append("'").append(this->compilation_time).append("'"); + out.append("\n"); + + out.append(" model: "); + out.append("'").append(this->model).append("'"); + out.append("\n"); + + out.append(" has_deep_sleep: "); + out.append(YESNO(this->has_deep_sleep)); + out.append("\n"); + out.append("}"); +} +void ListEntitiesRequest::encode(ProtoWriteBuffer buffer) const {} +void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } +void ListEntitiesDoneResponse::encode(ProtoWriteBuffer buffer) const {} +void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } +void SubscribeStatesRequest::encode(ProtoWriteBuffer buffer) const {} +void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); } +bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->is_status_binary_sensor = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesBinarySensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->device_class = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesBinarySensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->device_class); + buffer.encode_bool(6, this->is_status_binary_sensor); +} +void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesBinarySensorResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" is_status_binary_sensor: "); + out.append(YESNO(this->is_status_binary_sensor)); + out.append("\n"); + out.append("}"); +} +bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool BinarySensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->state); +} +void BinarySensorStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("BinarySensorStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + out.append("}"); +} +bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 5: { + this->assumed_state = value.as_bool(); + return true; + } + case 6: { + this->supports_position = value.as_bool(); + return true; + } + case 7: { + this->supports_tilt = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesCoverResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 8: { + this->device_class = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesCoverResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_bool(5, this->assumed_state); + buffer.encode_bool(6, this->supports_position); + buffer.encode_bool(7, this->supports_tilt); + buffer.encode_string(8, this->device_class); +} +void ListEntitiesCoverResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesCoverResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" assumed_state: "); + out.append(YESNO(this->assumed_state)); + out.append("\n"); + + out.append(" supports_position: "); + out.append(YESNO(this->supports_position)); + out.append("\n"); + + out.append(" supports_tilt: "); + out.append(YESNO(this->supports_tilt)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + out.append("}"); +} +bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->legacy_state = value.as_enum<EnumLegacyCoverState>(); + return true; + } + case 5: { + this->current_operation = value.as_enum<EnumCoverOperation>(); + return true; + } + default: + return false; + } +} +bool CoverStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 3: { + this->position = value.as_float(); + return true; + } + case 4: { + this->tilt = value.as_float(); + return true; + } + default: + return false; + } +} +void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum<EnumLegacyCoverState>(2, this->legacy_state); + buffer.encode_float(3, this->position); + buffer.encode_float(4, this->tilt); + buffer.encode_enum<EnumCoverOperation>(5, this->current_operation); +} +void CoverStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("CoverStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" legacy_state: "); + out.append(proto_enum_to_string<EnumLegacyCoverState>(this->legacy_state)); + out.append("\n"); + + out.append(" position: "); + sprintf(buffer, "%g", this->position); + out.append(buffer); + out.append("\n"); + + out.append(" tilt: "); + sprintf(buffer, "%g", this->tilt); + out.append(buffer); + out.append("\n"); + + out.append(" current_operation: "); + out.append(proto_enum_to_string<EnumCoverOperation>(this->current_operation)); + out.append("\n"); + out.append("}"); +} +bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->has_legacy_command = value.as_bool(); + return true; + } + case 3: { + this->legacy_command = value.as_enum<EnumLegacyCoverCommand>(); + return true; + } + case 4: { + this->has_position = value.as_bool(); + return true; + } + case 6: { + this->has_tilt = value.as_bool(); + return true; + } + case 8: { + this->stop = value.as_bool(); + return true; + } + default: + return false; + } +} +bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 5: { + this->position = value.as_float(); + return true; + } + case 7: { + this->tilt = value.as_float(); + return true; + } + default: + return false; + } +} +void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->has_legacy_command); + buffer.encode_enum<EnumLegacyCoverCommand>(3, this->legacy_command); + buffer.encode_bool(4, this->has_position); + buffer.encode_float(5, this->position); + buffer.encode_bool(6, this->has_tilt); + buffer.encode_float(7, this->tilt); + buffer.encode_bool(8, this->stop); +} +void CoverCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("CoverCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_legacy_command: "); + out.append(YESNO(this->has_legacy_command)); + out.append("\n"); + + out.append(" legacy_command: "); + out.append(proto_enum_to_string<EnumLegacyCoverCommand>(this->legacy_command)); + out.append("\n"); + + out.append(" has_position: "); + out.append(YESNO(this->has_position)); + out.append("\n"); + + out.append(" position: "); + sprintf(buffer, "%g", this->position); + out.append(buffer); + out.append("\n"); + + out.append(" has_tilt: "); + out.append(YESNO(this->has_tilt)); + out.append("\n"); + + out.append(" tilt: "); + sprintf(buffer, "%g", this->tilt); + out.append(buffer); + out.append("\n"); + + out.append(" stop: "); + out.append(YESNO(this->stop)); + out.append("\n"); + out.append("}"); +} +bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 5: { + this->supports_oscillation = value.as_bool(); + return true; + } + case 6: { + this->supports_speed = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesFanResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_bool(5, this->supports_oscillation); + buffer.encode_bool(6, this->supports_speed); +} +void ListEntitiesFanResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesFanResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" supports_oscillation: "); + out.append(YESNO(this->supports_oscillation)); + out.append("\n"); + + out.append(" supports_speed: "); + out.append(YESNO(this->supports_speed)); + out.append("\n"); + out.append("}"); +} +bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_bool(); + return true; + } + case 3: { + this->oscillating = value.as_bool(); + return true; + } + case 4: { + this->speed = value.as_enum<EnumFanSpeed>(); + return true; + } + default: + return false; + } +} +bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void FanStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->state); + buffer.encode_bool(3, this->oscillating); + buffer.encode_enum<EnumFanSpeed>(4, this->speed); +} +void FanStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("FanStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" oscillating: "); + out.append(YESNO(this->oscillating)); + out.append("\n"); + + out.append(" speed: "); + out.append(proto_enum_to_string<EnumFanSpeed>(this->speed)); + out.append("\n"); + out.append("}"); +} +bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->has_state = value.as_bool(); + return true; + } + case 3: { + this->state = value.as_bool(); + return true; + } + case 4: { + this->has_speed = value.as_bool(); + return true; + } + case 5: { + this->speed = value.as_enum<EnumFanSpeed>(); + return true; + } + case 6: { + this->has_oscillating = value.as_bool(); + return true; + } + case 7: { + this->oscillating = value.as_bool(); + return true; + } + default: + return false; + } +} +bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->has_state); + buffer.encode_bool(3, this->state); + buffer.encode_bool(4, this->has_speed); + buffer.encode_enum<EnumFanSpeed>(5, this->speed); + buffer.encode_bool(6, this->has_oscillating); + buffer.encode_bool(7, this->oscillating); +} +void FanCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("FanCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_state: "); + out.append(YESNO(this->has_state)); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" has_speed: "); + out.append(YESNO(this->has_speed)); + out.append("\n"); + + out.append(" speed: "); + out.append(proto_enum_to_string<EnumFanSpeed>(this->speed)); + out.append("\n"); + + out.append(" has_oscillating: "); + out.append(YESNO(this->has_oscillating)); + out.append("\n"); + + out.append(" oscillating: "); + out.append(YESNO(this->oscillating)); + out.append("\n"); + out.append("}"); +} +bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 5: { + this->supports_brightness = value.as_bool(); + return true; + } + case 6: { + this->supports_rgb = value.as_bool(); + return true; + } + case 7: { + this->supports_white_value = value.as_bool(); + return true; + } + case 8: { + this->supports_color_temperature = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesLightResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 11: { + this->effects.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +bool ListEntitiesLightResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + case 9: { + this->min_mireds = value.as_float(); + return true; + } + case 10: { + this->max_mireds = value.as_float(); + return true; + } + default: + return false; + } +} +void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_bool(5, this->supports_brightness); + buffer.encode_bool(6, this->supports_rgb); + buffer.encode_bool(7, this->supports_white_value); + buffer.encode_bool(8, this->supports_color_temperature); + buffer.encode_float(9, this->min_mireds); + buffer.encode_float(10, this->max_mireds); + for (auto &it : this->effects) { + buffer.encode_string(11, it, true); + } +} +void ListEntitiesLightResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesLightResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" supports_brightness: "); + out.append(YESNO(this->supports_brightness)); + out.append("\n"); + + out.append(" supports_rgb: "); + out.append(YESNO(this->supports_rgb)); + out.append("\n"); + + out.append(" supports_white_value: "); + out.append(YESNO(this->supports_white_value)); + out.append("\n"); + + out.append(" supports_color_temperature: "); + out.append(YESNO(this->supports_color_temperature)); + out.append("\n"); + + out.append(" min_mireds: "); + sprintf(buffer, "%g", this->min_mireds); + out.append(buffer); + out.append("\n"); + + out.append(" max_mireds: "); + sprintf(buffer, "%g", this->max_mireds); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->effects) { + out.append(" effects: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool LightStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 9: { + this->effect = value.as_string(); + return true; + } + default: + return false; + } +} +bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 3: { + this->brightness = value.as_float(); + return true; + } + case 4: { + this->red = value.as_float(); + return true; + } + case 5: { + this->green = value.as_float(); + return true; + } + case 6: { + this->blue = value.as_float(); + return true; + } + case 7: { + this->white = value.as_float(); + return true; + } + case 8: { + this->color_temperature = value.as_float(); + return true; + } + default: + return false; + } +} +void LightStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->state); + buffer.encode_float(3, this->brightness); + buffer.encode_float(4, this->red); + buffer.encode_float(5, this->green); + buffer.encode_float(6, this->blue); + buffer.encode_float(7, this->white); + buffer.encode_float(8, this->color_temperature); + buffer.encode_string(9, this->effect); +} +void LightStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("LightStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" brightness: "); + sprintf(buffer, "%g", this->brightness); + out.append(buffer); + out.append("\n"); + + out.append(" red: "); + sprintf(buffer, "%g", this->red); + out.append(buffer); + out.append("\n"); + + out.append(" green: "); + sprintf(buffer, "%g", this->green); + out.append(buffer); + out.append("\n"); + + out.append(" blue: "); + sprintf(buffer, "%g", this->blue); + out.append(buffer); + out.append("\n"); + + out.append(" white: "); + sprintf(buffer, "%g", this->white); + out.append(buffer); + out.append("\n"); + + out.append(" color_temperature: "); + sprintf(buffer, "%g", this->color_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" effect: "); + out.append("'").append(this->effect).append("'"); + out.append("\n"); + out.append("}"); +} +bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->has_state = value.as_bool(); + return true; + } + case 3: { + this->state = value.as_bool(); + return true; + } + case 4: { + this->has_brightness = value.as_bool(); + return true; + } + case 6: { + this->has_rgb = value.as_bool(); + return true; + } + case 10: { + this->has_white = value.as_bool(); + return true; + } + case 12: { + this->has_color_temperature = value.as_bool(); + return true; + } + case 14: { + this->has_transition_length = value.as_bool(); + return true; + } + case 15: { + this->transition_length = value.as_uint32(); + return true; + } + case 16: { + this->has_flash_length = value.as_bool(); + return true; + } + case 17: { + this->flash_length = value.as_uint32(); + return true; + } + case 18: { + this->has_effect = value.as_bool(); + return true; + } + default: + return false; + } +} +bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 19: { + this->effect = value.as_string(); + return true; + } + default: + return false; + } +} +bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 5: { + this->brightness = value.as_float(); + return true; + } + case 7: { + this->red = value.as_float(); + return true; + } + case 8: { + this->green = value.as_float(); + return true; + } + case 9: { + this->blue = value.as_float(); + return true; + } + case 11: { + this->white = value.as_float(); + return true; + } + case 13: { + this->color_temperature = value.as_float(); + return true; + } + default: + return false; + } +} +void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->has_state); + buffer.encode_bool(3, this->state); + buffer.encode_bool(4, this->has_brightness); + buffer.encode_float(5, this->brightness); + buffer.encode_bool(6, this->has_rgb); + buffer.encode_float(7, this->red); + buffer.encode_float(8, this->green); + buffer.encode_float(9, this->blue); + buffer.encode_bool(10, this->has_white); + buffer.encode_float(11, this->white); + buffer.encode_bool(12, this->has_color_temperature); + buffer.encode_float(13, this->color_temperature); + buffer.encode_bool(14, this->has_transition_length); + buffer.encode_uint32(15, this->transition_length); + buffer.encode_bool(16, this->has_flash_length); + buffer.encode_uint32(17, this->flash_length); + buffer.encode_bool(18, this->has_effect); + buffer.encode_string(19, this->effect); +} +void LightCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("LightCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_state: "); + out.append(YESNO(this->has_state)); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" has_brightness: "); + out.append(YESNO(this->has_brightness)); + out.append("\n"); + + out.append(" brightness: "); + sprintf(buffer, "%g", this->brightness); + out.append(buffer); + out.append("\n"); + + out.append(" has_rgb: "); + out.append(YESNO(this->has_rgb)); + out.append("\n"); + + out.append(" red: "); + sprintf(buffer, "%g", this->red); + out.append(buffer); + out.append("\n"); + + out.append(" green: "); + sprintf(buffer, "%g", this->green); + out.append(buffer); + out.append("\n"); + + out.append(" blue: "); + sprintf(buffer, "%g", this->blue); + out.append(buffer); + out.append("\n"); + + out.append(" has_white: "); + out.append(YESNO(this->has_white)); + out.append("\n"); + + out.append(" white: "); + sprintf(buffer, "%g", this->white); + out.append(buffer); + out.append("\n"); + + out.append(" has_color_temperature: "); + out.append(YESNO(this->has_color_temperature)); + out.append("\n"); + + out.append(" color_temperature: "); + sprintf(buffer, "%g", this->color_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" has_transition_length: "); + out.append(YESNO(this->has_transition_length)); + out.append("\n"); + + out.append(" transition_length: "); + sprintf(buffer, "%u", this->transition_length); + out.append(buffer); + out.append("\n"); + + out.append(" has_flash_length: "); + out.append(YESNO(this->has_flash_length)); + out.append("\n"); + + out.append(" flash_length: "); + sprintf(buffer, "%u", this->flash_length); + out.append(buffer); + out.append("\n"); + + out.append(" has_effect: "); + out.append(YESNO(this->has_effect)); + out.append("\n"); + + out.append(" effect: "); + out.append("'").append(this->effect).append("'"); + out.append("\n"); + out.append("}"); +} +bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 7: { + this->accuracy_decimals = value.as_int32(); + return true; + } + case 8: { + this->force_update = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + case 6: { + this->unit_of_measurement = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesSensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_string(6, this->unit_of_measurement); + buffer.encode_int32(7, this->accuracy_decimals); + buffer.encode_bool(8, this->force_update); +} +void ListEntitiesSensorResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesSensorResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" unit_of_measurement: "); + out.append("'").append(this->unit_of_measurement).append("'"); + out.append("\n"); + + out.append(" accuracy_decimals: "); + sprintf(buffer, "%d", this->accuracy_decimals); + out.append(buffer); + out.append("\n"); + + out.append(" force_update: "); + out.append(YESNO(this->force_update)); + out.append("\n"); + out.append("}"); +} +bool SensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 2: { + this->state = value.as_float(); + return true; + } + default: + return false; + } +} +void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_float(2, this->state); +} +void SensorStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("SensorStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + sprintf(buffer, "%g", this->state); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->assumed_state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesSwitchResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesSwitchResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->assumed_state); +} +void ListEntitiesSwitchResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesSwitchResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" assumed_state: "); + out.append(YESNO(this->assumed_state)); + out.append("\n"); + out.append("}"); +} +bool SwitchStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool SwitchStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->state); +} +void SwitchStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("SwitchStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + out.append("}"); +} +bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->state); +} +void SwitchCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("SwitchCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + out.append("}"); +} +bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesTextSensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); +} +void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesTextSensorResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + out.append("}"); +} +bool TextSensorStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->state = value.as_string(); + return true; + } + default: + return false; + } +} +bool TextSensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->state); +} +void TextSensorStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("TextSensorStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + out.append("}"); +} +bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->level = value.as_enum<EnumLogLevel>(); + return true; + } + case 2: { + this->dump_config = value.as_bool(); + return true; + } + default: + return false; + } +} +void SubscribeLogsRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_enum<EnumLogLevel>(1, this->level); + buffer.encode_bool(2, this->dump_config); +} +void SubscribeLogsRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("SubscribeLogsRequest {\n"); + out.append(" level: "); + out.append(proto_enum_to_string<EnumLogLevel>(this->level)); + out.append("\n"); + + out.append(" dump_config: "); + out.append(YESNO(this->dump_config)); + out.append("\n"); + out.append("}"); +} +bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->level = value.as_enum<EnumLogLevel>(); + return true; + } + case 4: { + this->send_failed = value.as_bool(); + return true; + } + default: + return false; + } +} +bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->tag = value.as_string(); + return true; + } + case 3: { + this->message = value.as_string(); + return true; + } + default: + return false; + } +} +void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_enum<EnumLogLevel>(1, this->level); + buffer.encode_string(2, this->tag); + buffer.encode_string(3, this->message); + buffer.encode_bool(4, this->send_failed); +} +void SubscribeLogsResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("SubscribeLogsResponse {\n"); + out.append(" level: "); + out.append(proto_enum_to_string<EnumLogLevel>(this->level)); + out.append("\n"); + + out.append(" tag: "); + out.append("'").append(this->tag).append("'"); + out.append("\n"); + + out.append(" message: "); + out.append("'").append(this->message).append("'"); + out.append("\n"); + + out.append(" send_failed: "); + out.append(YESNO(this->send_failed)); + out.append("\n"); + out.append("}"); +} +void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {} +void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { + out.append("SubscribeHomeassistantServicesRequest {}"); +} +bool HomeassistantServiceMap::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->key = value.as_string(); + return true; + } + case 2: { + this->value = value.as_string(); + return true; + } + default: + return false; + } +} +void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->key); + buffer.encode_string(2, this->value); +} +void HomeassistantServiceMap::dump_to(std::string &out) const { + char buffer[64]; + out.append("HomeassistantServiceMap {\n"); + out.append(" key: "); + out.append("'").append(this->key).append("'"); + out.append("\n"); + + out.append(" value: "); + out.append("'").append(this->value).append("'"); + out.append("\n"); + out.append("}"); +} +bool HomeassistantServiceResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 5: { + this->is_event = value.as_bool(); + return true; + } + default: + return false; + } +} +bool HomeassistantServiceResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->service = value.as_string(); + return true; + } + case 2: { + this->data.push_back(value.as_message<HomeassistantServiceMap>()); + return true; + } + case 3: { + this->data_template.push_back(value.as_message<HomeassistantServiceMap>()); + return true; + } + case 4: { + this->variables.push_back(value.as_message<HomeassistantServiceMap>()); + return true; + } + default: + return false; + } +} +void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->service); + for (auto &it : this->data) { + buffer.encode_message<HomeassistantServiceMap>(2, it, true); + } + for (auto &it : this->data_template) { + buffer.encode_message<HomeassistantServiceMap>(3, it, true); + } + for (auto &it : this->variables) { + buffer.encode_message<HomeassistantServiceMap>(4, it, true); + } + buffer.encode_bool(5, this->is_event); +} +void HomeassistantServiceResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("HomeassistantServiceResponse {\n"); + out.append(" service: "); + out.append("'").append(this->service).append("'"); + out.append("\n"); + + for (const auto &it : this->data) { + out.append(" data: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->data_template) { + out.append(" data_template: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->variables) { + out.append(" variables: "); + it.dump_to(out); + out.append("\n"); + } + + out.append(" is_event: "); + out.append(YESNO(this->is_event)); + out.append("\n"); + out.append("}"); +} +void SubscribeHomeAssistantStatesRequest::encode(ProtoWriteBuffer buffer) const {} +void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { + out.append("SubscribeHomeAssistantStatesRequest {}"); +} +bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->entity_id = value.as_string(); + return true; + } + default: + return false; + } +} +void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->entity_id); +} +void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("SubscribeHomeAssistantStateResponse {\n"); + out.append(" entity_id: "); + out.append("'").append(this->entity_id).append("'"); + out.append("\n"); + out.append("}"); +} +bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->entity_id = value.as_string(); + return true; + } + case 2: { + this->state = value.as_string(); + return true; + } + default: + return false; + } +} +void HomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->entity_id); + buffer.encode_string(2, this->state); +} +void HomeAssistantStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("HomeAssistantStateResponse {\n"); + out.append(" entity_id: "); + out.append("'").append(this->entity_id).append("'"); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + out.append("}"); +} +void GetTimeRequest::encode(ProtoWriteBuffer buffer) const {} +void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } +bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->epoch_seconds = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } +void GetTimeResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("GetTimeResponse {\n"); + out.append(" epoch_seconds: "); + sprintf(buffer, "%u", this->epoch_seconds); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->type = value.as_enum<EnumServiceArgType>(); + return true; + } + default: + return false; + } +} +bool ListEntitiesServicesArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->name = value.as_string(); + return true; + } + default: + return false; + } +} +void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->name); + buffer.encode_enum<EnumServiceArgType>(2, this->type); +} +void ListEntitiesServicesArgument::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesServicesArgument {\n"); + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" type: "); + out.append(proto_enum_to_string<EnumServiceArgType>(this->type)); + out.append("\n"); + out.append("}"); +} +bool ListEntitiesServicesResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->name = value.as_string(); + return true; + } + case 3: { + this->args.push_back(value.as_message<ListEntitiesServicesArgument>()); + return true; + } + default: + return false; + } +} +bool ListEntitiesServicesResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->name); + buffer.encode_fixed32(2, this->key); + for (auto &it : this->args) { + buffer.encode_message<ListEntitiesServicesArgument>(3, it, true); + } +} +void ListEntitiesServicesResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesServicesResponse {\n"); + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->args) { + out.append(" args: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->bool_ = value.as_bool(); + return true; + } + case 2: { + this->legacy_int = value.as_int32(); + return true; + } + case 5: { + this->int_ = value.as_sint32(); + return true; + } + case 6: { + this->bool_array.push_back(value.as_bool()); + return true; + } + case 7: { + this->int_array.push_back(value.as_sint32()); + return true; + } + default: + return false; + } +} +bool ExecuteServiceArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 4: { + this->string_ = value.as_string(); + return true; + } + case 9: { + this->string_array.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 3: { + this->float_ = value.as_float(); + return true; + } + case 8: { + this->float_array.push_back(value.as_float()); + return true; + } + default: + return false; + } +} +void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const { + buffer.encode_bool(1, this->bool_); + buffer.encode_int32(2, this->legacy_int); + buffer.encode_float(3, this->float_); + buffer.encode_string(4, this->string_); + buffer.encode_sint32(5, this->int_); + for (auto it : this->bool_array) { + buffer.encode_bool(6, it, true); + } + for (auto &it : this->int_array) { + buffer.encode_sint32(7, it, true); + } + for (auto &it : this->float_array) { + buffer.encode_float(8, it, true); + } + for (auto &it : this->string_array) { + buffer.encode_string(9, it, true); + } +} +void ExecuteServiceArgument::dump_to(std::string &out) const { + char buffer[64]; + out.append("ExecuteServiceArgument {\n"); + out.append(" bool_: "); + out.append(YESNO(this->bool_)); + out.append("\n"); + + out.append(" legacy_int: "); + sprintf(buffer, "%d", this->legacy_int); + out.append(buffer); + out.append("\n"); + + out.append(" float_: "); + sprintf(buffer, "%g", this->float_); + out.append(buffer); + out.append("\n"); + + out.append(" string_: "); + out.append("'").append(this->string_).append("'"); + out.append("\n"); + + out.append(" int_: "); + sprintf(buffer, "%d", this->int_); + out.append(buffer); + out.append("\n"); + + for (const auto it : this->bool_array) { + out.append(" bool_array: "); + out.append(YESNO(it)); + out.append("\n"); + } + + for (const auto &it : this->int_array) { + out.append(" int_array: "); + sprintf(buffer, "%d", it); + out.append(buffer); + out.append("\n"); + } + + for (const auto &it : this->float_array) { + out.append(" float_array: "); + sprintf(buffer, "%g", it); + out.append(buffer); + out.append("\n"); + } + + for (const auto &it : this->string_array) { + out.append(" string_array: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->args.push_back(value.as_message<ExecuteServiceArgument>()); + return true; + } + default: + return false; + } +} +bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ExecuteServiceRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + for (auto &it : this->args) { + buffer.encode_message<ExecuteServiceArgument>(2, it, true); + } +} +void ExecuteServiceRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("ExecuteServiceRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->args) { + out.append(" args: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesCameraResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); +} +void ListEntitiesCameraResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesCameraResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + out.append("}"); +} +bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->done = value.as_bool(); + return true; + } + default: + return false; + } +} +bool CameraImageResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->data = value.as_string(); + return true; + } + default: + return false; + } +} +bool CameraImageResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->data); + buffer.encode_bool(3, this->done); +} +void CameraImageResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("CameraImageResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append("'").append(this->data).append("'"); + out.append("\n"); + + out.append(" done: "); + out.append(YESNO(this->done)); + out.append("\n"); + out.append("}"); +} +bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->single = value.as_bool(); + return true; + } + case 2: { + this->stream = value.as_bool(); + return true; + } + default: + return false; + } +} +void CameraImageRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_bool(1, this->single); + buffer.encode_bool(2, this->stream); +} +void CameraImageRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("CameraImageRequest {\n"); + out.append(" single: "); + out.append(YESNO(this->single)); + out.append("\n"); + + out.append(" stream: "); + out.append(YESNO(this->stream)); + out.append("\n"); + out.append("}"); +} +bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 5: { + this->supports_current_temperature = value.as_bool(); + return true; + } + case 6: { + this->supports_two_point_target_temperature = value.as_bool(); + return true; + } + case 7: { + this->supported_modes.push_back(value.as_enum<EnumClimateMode>()); + return true; + } + case 11: { + this->supports_away = value.as_bool(); + return true; + } + case 12: { + this->supports_action = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesClimateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + case 8: { + this->visual_min_temperature = value.as_float(); + return true; + } + case 9: { + this->visual_max_temperature = value.as_float(); + return true; + } + case 10: { + this->visual_temperature_step = value.as_float(); + return true; + } + default: + return false; + } +} +void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_bool(5, this->supports_current_temperature); + buffer.encode_bool(6, this->supports_two_point_target_temperature); + for (auto &it : this->supported_modes) { + buffer.encode_enum<EnumClimateMode>(7, it, true); + } + buffer.encode_float(8, this->visual_min_temperature); + buffer.encode_float(9, this->visual_max_temperature); + buffer.encode_float(10, this->visual_temperature_step); + buffer.encode_bool(11, this->supports_away); + buffer.encode_bool(12, this->supports_action); +} +void ListEntitiesClimateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesClimateResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" supports_current_temperature: "); + out.append(YESNO(this->supports_current_temperature)); + out.append("\n"); + + out.append(" supports_two_point_target_temperature: "); + out.append(YESNO(this->supports_two_point_target_temperature)); + out.append("\n"); + + for (const auto &it : this->supported_modes) { + out.append(" supported_modes: "); + out.append(proto_enum_to_string<EnumClimateMode>(it)); + out.append("\n"); + } + + out.append(" visual_min_temperature: "); + sprintf(buffer, "%g", this->visual_min_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" visual_max_temperature: "); + sprintf(buffer, "%g", this->visual_max_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" visual_temperature_step: "); + sprintf(buffer, "%g", this->visual_temperature_step); + out.append(buffer); + out.append("\n"); + + out.append(" supports_away: "); + out.append(YESNO(this->supports_away)); + out.append("\n"); + + out.append(" supports_action: "); + out.append(YESNO(this->supports_action)); + out.append("\n"); + out.append("}"); +} +bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->mode = value.as_enum<EnumClimateMode>(); + return true; + } + case 7: { + this->away = value.as_bool(); + return true; + } + case 8: { + this->action = value.as_enum<EnumClimateAction>(); + return true; + } + default: + return false; + } +} +bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 3: { + this->current_temperature = value.as_float(); + return true; + } + case 4: { + this->target_temperature = value.as_float(); + return true; + } + case 5: { + this->target_temperature_low = value.as_float(); + return true; + } + case 6: { + this->target_temperature_high = value.as_float(); + return true; + } + default: + return false; + } +} +void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum<EnumClimateMode>(2, this->mode); + buffer.encode_float(3, this->current_temperature); + buffer.encode_float(4, this->target_temperature); + buffer.encode_float(5, this->target_temperature_low); + buffer.encode_float(6, this->target_temperature_high); + buffer.encode_bool(7, this->away); + buffer.encode_enum<EnumClimateAction>(8, this->action); +} +void ClimateStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ClimateStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" mode: "); + out.append(proto_enum_to_string<EnumClimateMode>(this->mode)); + out.append("\n"); + + out.append(" current_temperature: "); + sprintf(buffer, "%g", this->current_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" target_temperature: "); + sprintf(buffer, "%g", this->target_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" target_temperature_low: "); + sprintf(buffer, "%g", this->target_temperature_low); + out.append(buffer); + out.append("\n"); + + out.append(" target_temperature_high: "); + sprintf(buffer, "%g", this->target_temperature_high); + out.append(buffer); + out.append("\n"); + + out.append(" away: "); + out.append(YESNO(this->away)); + out.append("\n"); + + out.append(" action: "); + out.append(proto_enum_to_string<EnumClimateAction>(this->action)); + out.append("\n"); + out.append("}"); +} +bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->has_mode = value.as_bool(); + return true; + } + case 3: { + this->mode = value.as_enum<EnumClimateMode>(); + return true; + } + case 4: { + this->has_target_temperature = value.as_bool(); + return true; + } + case 6: { + this->has_target_temperature_low = value.as_bool(); + return true; + } + case 8: { + this->has_target_temperature_high = value.as_bool(); + return true; + } + case 10: { + this->has_away = value.as_bool(); + return true; + } + case 11: { + this->away = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 5: { + this->target_temperature = value.as_float(); + return true; + } + case 7: { + this->target_temperature_low = value.as_float(); + return true; + } + case 9: { + this->target_temperature_high = value.as_float(); + return true; + } + default: + return false; + } +} +void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->has_mode); + buffer.encode_enum<EnumClimateMode>(3, this->mode); + buffer.encode_bool(4, this->has_target_temperature); + buffer.encode_float(5, this->target_temperature); + buffer.encode_bool(6, this->has_target_temperature_low); + buffer.encode_float(7, this->target_temperature_low); + buffer.encode_bool(8, this->has_target_temperature_high); + buffer.encode_float(9, this->target_temperature_high); + buffer.encode_bool(10, this->has_away); + buffer.encode_bool(11, this->away); +} +void ClimateCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("ClimateCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_mode: "); + out.append(YESNO(this->has_mode)); + out.append("\n"); + + out.append(" mode: "); + out.append(proto_enum_to_string<EnumClimateMode>(this->mode)); + out.append("\n"); + + out.append(" has_target_temperature: "); + out.append(YESNO(this->has_target_temperature)); + out.append("\n"); + + out.append(" target_temperature: "); + sprintf(buffer, "%g", this->target_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" has_target_temperature_low: "); + out.append(YESNO(this->has_target_temperature_low)); + out.append("\n"); + + out.append(" target_temperature_low: "); + sprintf(buffer, "%g", this->target_temperature_low); + out.append(buffer); + out.append("\n"); + + out.append(" has_target_temperature_high: "); + out.append(YESNO(this->has_target_temperature_high)); + out.append("\n"); + + out.append(" target_temperature_high: "); + sprintf(buffer, "%g", this->target_temperature_high); + out.append(buffer); + out.append("\n"); + + out.append(" has_away: "); + out.append(YESNO(this->has_away)); + out.append("\n"); + + out.append(" away: "); + out.append(YESNO(this->away)); + out.append("\n"); + out.append("}"); +} + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h new file mode 100644 index 0000000000..2e685959bf --- /dev/null +++ b/esphome/components/api/api_pb2.h @@ -0,0 +1,695 @@ +#pragma once + +#include "proto.h" + +namespace esphome { +namespace api { + +enum EnumLegacyCoverState : uint32_t { + LEGACY_COVER_STATE_OPEN = 0, + LEGACY_COVER_STATE_CLOSED = 1, +}; +enum EnumCoverOperation : uint32_t { + COVER_OPERATION_IDLE = 0, + COVER_OPERATION_IS_OPENING = 1, + COVER_OPERATION_IS_CLOSING = 2, +}; +enum EnumLegacyCoverCommand : uint32_t { + LEGACY_COVER_COMMAND_OPEN = 0, + LEGACY_COVER_COMMAND_CLOSE = 1, + LEGACY_COVER_COMMAND_STOP = 2, +}; +enum EnumFanSpeed : uint32_t { + FAN_SPEED_LOW = 0, + FAN_SPEED_MEDIUM = 1, + FAN_SPEED_HIGH = 2, +}; +enum EnumLogLevel : uint32_t { + LOG_LEVEL_NONE = 0, + LOG_LEVEL_ERROR = 1, + LOG_LEVEL_WARN = 2, + LOG_LEVEL_INFO = 3, + LOG_LEVEL_DEBUG = 4, + LOG_LEVEL_VERBOSE = 5, + LOG_LEVEL_VERY_VERBOSE = 6, +}; +enum EnumServiceArgType : uint32_t { + SERVICE_ARG_TYPE_BOOL = 0, + SERVICE_ARG_TYPE_INT = 1, + SERVICE_ARG_TYPE_FLOAT = 2, + SERVICE_ARG_TYPE_STRING = 3, + SERVICE_ARG_TYPE_BOOL_ARRAY = 4, + SERVICE_ARG_TYPE_INT_ARRAY = 5, + SERVICE_ARG_TYPE_FLOAT_ARRAY = 6, + SERVICE_ARG_TYPE_STRING_ARRAY = 7, +}; +enum EnumClimateMode : uint32_t { + CLIMATE_MODE_OFF = 0, + CLIMATE_MODE_AUTO = 1, + CLIMATE_MODE_COOL = 2, + CLIMATE_MODE_HEAT = 3, +}; +enum EnumClimateAction : uint32_t { + CLIMATE_ACTION_OFF = 0, + CLIMATE_ACTION_COOLING = 2, + CLIMATE_ACTION_HEATING = 3, +}; +class HelloRequest : public ProtoMessage { + public: + std::string client_info{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class HelloResponse : public ProtoMessage { + public: + uint32_t api_version_major{0}; // NOLINT + uint32_t api_version_minor{0}; // NOLINT + std::string server_info{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ConnectRequest : public ProtoMessage { + public: + std::string password{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class ConnectResponse : public ProtoMessage { + public: + bool invalid_password{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class DisconnectRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class DisconnectResponse : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class PingRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class PingResponse : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class DeviceInfoRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class DeviceInfoResponse : public ProtoMessage { + public: + bool uses_password{false}; // NOLINT + std::string name{}; // NOLINT + std::string mac_address{}; // NOLINT + std::string esphome_version{}; // NOLINT + std::string compilation_time{}; // NOLINT + std::string model{}; // NOLINT + bool has_deep_sleep{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ListEntitiesRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class ListEntitiesDoneResponse : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class SubscribeStatesRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class ListEntitiesBinarySensorResponse : public ProtoMessage { + public: + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + std::string device_class{}; // NOLINT + bool is_status_binary_sensor{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BinarySensorStateResponse : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + bool state{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ListEntitiesCoverResponse : public ProtoMessage { + public: + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + bool assumed_state{false}; // NOLINT + bool supports_position{false}; // NOLINT + bool supports_tilt{false}; // NOLINT + std::string device_class{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class CoverStateResponse : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + EnumLegacyCoverState legacy_state{}; // NOLINT + float position{0.0f}; // NOLINT + float tilt{0.0f}; // NOLINT + EnumCoverOperation current_operation{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class CoverCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + bool has_legacy_command{false}; // NOLINT + EnumLegacyCoverCommand legacy_command{}; // NOLINT + bool has_position{false}; // NOLINT + float position{0.0f}; // NOLINT + bool has_tilt{false}; // NOLINT + float tilt{0.0f}; // NOLINT + bool stop{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ListEntitiesFanResponse : public ProtoMessage { + public: + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + bool supports_oscillation{false}; // NOLINT + bool supports_speed{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class FanStateResponse : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + bool state{false}; // NOLINT + bool oscillating{false}; // NOLINT + EnumFanSpeed speed{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class FanCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + bool has_state{false}; // NOLINT + bool state{false}; // NOLINT + bool has_speed{false}; // NOLINT + EnumFanSpeed speed{}; // NOLINT + bool has_oscillating{false}; // NOLINT + bool oscillating{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ListEntitiesLightResponse : public ProtoMessage { + public: + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + bool supports_brightness{false}; // NOLINT + bool supports_rgb{false}; // NOLINT + bool supports_white_value{false}; // NOLINT + bool supports_color_temperature{false}; // NOLINT + float min_mireds{0.0f}; // NOLINT + float max_mireds{0.0f}; // NOLINT + std::vector<std::string> effects{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class LightStateResponse : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + bool state{false}; // NOLINT + float brightness{0.0f}; // NOLINT + float red{0.0f}; // NOLINT + float green{0.0f}; // NOLINT + float blue{0.0f}; // NOLINT + float white{0.0f}; // NOLINT + float color_temperature{0.0f}; // NOLINT + std::string effect{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class LightCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + bool has_state{false}; // NOLINT + bool state{false}; // NOLINT + bool has_brightness{false}; // NOLINT + float brightness{0.0f}; // NOLINT + bool has_rgb{false}; // NOLINT + float red{0.0f}; // NOLINT + float green{0.0f}; // NOLINT + float blue{0.0f}; // NOLINT + bool has_white{false}; // NOLINT + float white{0.0f}; // NOLINT + bool has_color_temperature{false}; // NOLINT + float color_temperature{0.0f}; // NOLINT + bool has_transition_length{false}; // NOLINT + uint32_t transition_length{0}; // NOLINT + bool has_flash_length{false}; // NOLINT + uint32_t flash_length{0}; // NOLINT + bool has_effect{false}; // NOLINT + std::string effect{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ListEntitiesSensorResponse : public ProtoMessage { + public: + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + std::string icon{}; // NOLINT + std::string unit_of_measurement{}; // NOLINT + int32_t accuracy_decimals{0}; // NOLINT + bool force_update{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SensorStateResponse : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + float state{0.0f}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; +}; +class ListEntitiesSwitchResponse : public ProtoMessage { + public: + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + std::string icon{}; // NOLINT + bool assumed_state{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SwitchStateResponse : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + bool state{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SwitchCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + bool state{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ListEntitiesTextSensorResponse : public ProtoMessage { + public: + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + std::string icon{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class TextSensorStateResponse : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + std::string state{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class SubscribeLogsRequest : public ProtoMessage { + public: + EnumLogLevel level{}; // NOLINT + bool dump_config{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SubscribeLogsResponse : public ProtoMessage { + public: + EnumLogLevel level{}; // NOLINT + std::string tag{}; // NOLINT + std::string message{}; // NOLINT + bool send_failed{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SubscribeHomeassistantServicesRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class HomeassistantServiceMap : public ProtoMessage { + public: + std::string key{}; // NOLINT + std::string value{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class HomeassistantServiceResponse : public ProtoMessage { + public: + std::string service{}; // NOLINT + std::vector<HomeassistantServiceMap> data{}; // NOLINT + std::vector<HomeassistantServiceMap> data_template{}; // NOLINT + std::vector<HomeassistantServiceMap> variables{}; // NOLINT + bool is_event{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SubscribeHomeAssistantStatesRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class SubscribeHomeAssistantStateResponse : public ProtoMessage { + public: + std::string entity_id{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class HomeAssistantStateResponse : public ProtoMessage { + public: + std::string entity_id{}; // NOLINT + std::string state{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class GetTimeRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: +}; +class GetTimeResponse : public ProtoMessage { + public: + uint32_t epoch_seconds{0}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; +}; +class ListEntitiesServicesArgument : public ProtoMessage { + public: + std::string name{}; // NOLINT + EnumServiceArgType type{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ListEntitiesServicesResponse : public ProtoMessage { + public: + std::string name{}; // NOLINT + uint32_t key{0}; // NOLINT + std::vector<ListEntitiesServicesArgument> args{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class ExecuteServiceArgument : public ProtoMessage { + public: + bool bool_{false}; // NOLINT + int32_t legacy_int{0}; // NOLINT + float float_{0.0f}; // NOLINT + std::string string_{}; // NOLINT + int32_t int_{0}; // NOLINT + std::vector<bool> bool_array{}; // NOLINT + std::vector<int32_t> int_array{}; // NOLINT + std::vector<float> float_array{}; // NOLINT + std::vector<std::string> string_array{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ExecuteServiceRequest : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + std::vector<ExecuteServiceArgument> args{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class ListEntitiesCameraResponse : public ProtoMessage { + public: + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class CameraImageResponse : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + std::string data{}; // NOLINT + bool done{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class CameraImageRequest : public ProtoMessage { + public: + bool single{false}; // NOLINT + bool stream{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ListEntitiesClimateResponse : public ProtoMessage { + public: + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + bool supports_current_temperature{false}; // NOLINT + bool supports_two_point_target_temperature{false}; // NOLINT + std::vector<EnumClimateMode> supported_modes{}; // NOLINT + float visual_min_temperature{0.0f}; // NOLINT + float visual_max_temperature{0.0f}; // NOLINT + float visual_temperature_step{0.0f}; // NOLINT + bool supports_away{false}; // NOLINT + bool supports_action{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ClimateStateResponse : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + EnumClimateMode mode{}; // NOLINT + float current_temperature{0.0f}; // NOLINT + float target_temperature{0.0f}; // NOLINT + float target_temperature_low{0.0f}; // NOLINT + float target_temperature_high{0.0f}; // NOLINT + bool away{false}; // NOLINT + EnumClimateAction action{}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class ClimateCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; // NOLINT + bool has_mode{false}; // NOLINT + EnumClimateMode mode{}; // NOLINT + bool has_target_temperature{false}; // NOLINT + float target_temperature{0.0f}; // NOLINT + bool has_target_temperature_low{false}; // NOLINT + float target_temperature_low{0.0f}; // NOLINT + bool has_target_temperature_high{false}; // NOLINT + float target_temperature_high{0.0f}; // NOLINT + bool has_away{false}; // NOLINT + bool away{false}; // NOLINT + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp new file mode 100644 index 0000000000..13e123c10f --- /dev/null +++ b/esphome/components/api/api_pb2_service.cpp @@ -0,0 +1,582 @@ +#include "api_pb2_service.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace api { + +static const char *TAG = "api.service"; + +bool APIServerConnectionBase::send_hello_response(const HelloResponse &msg) { + ESP_LOGVV(TAG, "send_hello_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<HelloResponse>(msg, 2); +} +bool APIServerConnectionBase::send_connect_response(const ConnectResponse &msg) { + ESP_LOGVV(TAG, "send_connect_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<ConnectResponse>(msg, 4); +} +bool APIServerConnectionBase::send_disconnect_request(const DisconnectRequest &msg) { + ESP_LOGVV(TAG, "send_disconnect_request: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<DisconnectRequest>(msg, 5); +} +bool APIServerConnectionBase::send_disconnect_response(const DisconnectResponse &msg) { + ESP_LOGVV(TAG, "send_disconnect_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<DisconnectResponse>(msg, 6); +} +bool APIServerConnectionBase::send_ping_request(const PingRequest &msg) { + ESP_LOGVV(TAG, "send_ping_request: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<PingRequest>(msg, 7); +} +bool APIServerConnectionBase::send_ping_response(const PingResponse &msg) { + ESP_LOGVV(TAG, "send_ping_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<PingResponse>(msg, 8); +} +bool APIServerConnectionBase::send_device_info_response(const DeviceInfoResponse &msg) { + ESP_LOGVV(TAG, "send_device_info_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<DeviceInfoResponse>(msg, 10); +} +bool APIServerConnectionBase::send_list_entities_done_response(const ListEntitiesDoneResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_done_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<ListEntitiesDoneResponse>(msg, 19); +} +#ifdef USE_BINARY_SENSOR +bool APIServerConnectionBase::send_list_entities_binary_sensor_response(const ListEntitiesBinarySensorResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_binary_sensor_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<ListEntitiesBinarySensorResponse>(msg, 12); +} +#endif +#ifdef USE_BINARY_SENSOR +bool APIServerConnectionBase::send_binary_sensor_state_response(const BinarySensorStateResponse &msg) { + ESP_LOGVV(TAG, "send_binary_sensor_state_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<BinarySensorStateResponse>(msg, 21); +} +#endif +#ifdef USE_COVER +bool APIServerConnectionBase::send_list_entities_cover_response(const ListEntitiesCoverResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_cover_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<ListEntitiesCoverResponse>(msg, 13); +} +#endif +#ifdef USE_COVER +bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse &msg) { + ESP_LOGVV(TAG, "send_cover_state_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<CoverStateResponse>(msg, 22); +} +#endif +#ifdef USE_COVER +#endif +#ifdef USE_FAN +bool APIServerConnectionBase::send_list_entities_fan_response(const ListEntitiesFanResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_fan_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<ListEntitiesFanResponse>(msg, 14); +} +#endif +#ifdef USE_FAN +bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &msg) { + ESP_LOGVV(TAG, "send_fan_state_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<FanStateResponse>(msg, 23); +} +#endif +#ifdef USE_FAN +#endif +#ifdef USE_LIGHT +bool APIServerConnectionBase::send_list_entities_light_response(const ListEntitiesLightResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_light_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<ListEntitiesLightResponse>(msg, 15); +} +#endif +#ifdef USE_LIGHT +bool APIServerConnectionBase::send_light_state_response(const LightStateResponse &msg) { + ESP_LOGVV(TAG, "send_light_state_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<LightStateResponse>(msg, 24); +} +#endif +#ifdef USE_LIGHT +#endif +#ifdef USE_SENSOR +bool APIServerConnectionBase::send_list_entities_sensor_response(const ListEntitiesSensorResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_sensor_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<ListEntitiesSensorResponse>(msg, 16); +} +#endif +#ifdef USE_SENSOR +bool APIServerConnectionBase::send_sensor_state_response(const SensorStateResponse &msg) { + ESP_LOGVV(TAG, "send_sensor_state_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<SensorStateResponse>(msg, 25); +} +#endif +#ifdef USE_SWITCH +bool APIServerConnectionBase::send_list_entities_switch_response(const ListEntitiesSwitchResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_switch_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<ListEntitiesSwitchResponse>(msg, 17); +} +#endif +#ifdef USE_SWITCH +bool APIServerConnectionBase::send_switch_state_response(const SwitchStateResponse &msg) { + ESP_LOGVV(TAG, "send_switch_state_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<SwitchStateResponse>(msg, 26); +} +#endif +#ifdef USE_SWITCH +#endif +#ifdef USE_TEXT_SENSOR +bool APIServerConnectionBase::send_list_entities_text_sensor_response(const ListEntitiesTextSensorResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_text_sensor_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<ListEntitiesTextSensorResponse>(msg, 18); +} +#endif +#ifdef USE_TEXT_SENSOR +bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorStateResponse &msg) { + ESP_LOGVV(TAG, "send_text_sensor_state_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<TextSensorStateResponse>(msg, 27); +} +#endif +bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsResponse &msg) { + this->set_nodelay(false); + return this->send_message_<SubscribeLogsResponse>(msg, 29); +} +bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) { + ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<HomeassistantServiceResponse>(msg, 35); +} +bool APIServerConnectionBase::send_subscribe_home_assistant_state_response( + const SubscribeHomeAssistantStateResponse &msg) { + ESP_LOGVV(TAG, "send_subscribe_home_assistant_state_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<SubscribeHomeAssistantStateResponse>(msg, 39); +} +bool APIServerConnectionBase::send_get_time_request(const GetTimeRequest &msg) { + ESP_LOGVV(TAG, "send_get_time_request: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<GetTimeRequest>(msg, 36); +} +bool APIServerConnectionBase::send_get_time_response(const GetTimeResponse &msg) { + ESP_LOGVV(TAG, "send_get_time_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<GetTimeResponse>(msg, 37); +} +bool APIServerConnectionBase::send_list_entities_services_response(const ListEntitiesServicesResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_services_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<ListEntitiesServicesResponse>(msg, 41); +} +#ifdef USE_ESP32_CAMERA +bool APIServerConnectionBase::send_list_entities_camera_response(const ListEntitiesCameraResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_camera_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<ListEntitiesCameraResponse>(msg, 43); +} +#endif +#ifdef USE_ESP32_CAMERA +bool APIServerConnectionBase::send_camera_image_response(const CameraImageResponse &msg) { + ESP_LOGVV(TAG, "send_camera_image_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<CameraImageResponse>(msg, 44); +} +#endif +#ifdef USE_ESP32_CAMERA +#endif +#ifdef USE_CLIMATE +bool APIServerConnectionBase::send_list_entities_climate_response(const ListEntitiesClimateResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_climate_response: %s", msg.dump().c_str()); + this->set_nodelay(false); + return this->send_message_<ListEntitiesClimateResponse>(msg, 46); +} +#endif +#ifdef USE_CLIMATE +bool APIServerConnectionBase::send_climate_state_response(const ClimateStateResponse &msg) { + ESP_LOGVV(TAG, "send_climate_state_response: %s", msg.dump().c_str()); + this->set_nodelay(true); + return this->send_message_<ClimateStateResponse>(msg, 47); +} +#endif +#ifdef USE_CLIMATE +#endif +bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { + switch (msg_type) { + case 1: { + HelloRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_hello_request: %s", msg.dump().c_str()); + this->on_hello_request(msg); + break; + } + case 3: { + ConnectRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_connect_request: %s", msg.dump().c_str()); + this->on_connect_request(msg); + break; + } + case 5: { + DisconnectRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str()); + this->on_disconnect_request(msg); + break; + } + case 6: { + DisconnectResponse msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str()); + this->on_disconnect_response(msg); + break; + } + case 7: { + PingRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str()); + this->on_ping_request(msg); + break; + } + case 8: { + PingResponse msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str()); + this->on_ping_response(msg); + break; + } + case 9: { + DeviceInfoRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str()); + this->on_device_info_request(msg); + break; + } + case 11: { + ListEntitiesRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str()); + this->on_list_entities_request(msg); + break; + } + case 20: { + SubscribeStatesRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str()); + this->on_subscribe_states_request(msg); + break; + } + case 28: { + SubscribeLogsRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_subscribe_logs_request: %s", msg.dump().c_str()); + this->on_subscribe_logs_request(msg); + break; + } + case 30: { +#ifdef USE_COVER + CoverCommandRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str()); + this->on_cover_command_request(msg); +#endif + break; + } + case 31: { +#ifdef USE_FAN + FanCommandRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str()); + this->on_fan_command_request(msg); +#endif + break; + } + case 32: { +#ifdef USE_LIGHT + LightCommandRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str()); + this->on_light_command_request(msg); +#endif + break; + } + case 33: { +#ifdef USE_SWITCH + SwitchCommandRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str()); + this->on_switch_command_request(msg); +#endif + break; + } + case 34: { + SubscribeHomeassistantServicesRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str()); + this->on_subscribe_homeassistant_services_request(msg); + break; + } + case 36: { + GetTimeRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str()); + this->on_get_time_request(msg); + break; + } + case 37: { + GetTimeResponse msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_get_time_response: %s", msg.dump().c_str()); + this->on_get_time_response(msg); + break; + } + case 38: { + SubscribeHomeAssistantStatesRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str()); + this->on_subscribe_home_assistant_states_request(msg); + break; + } + case 40: { + HomeAssistantStateResponse msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_home_assistant_state_response: %s", msg.dump().c_str()); + this->on_home_assistant_state_response(msg); + break; + } + case 42: { + ExecuteServiceRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_execute_service_request: %s", msg.dump().c_str()); + this->on_execute_service_request(msg); + break; + } + case 45: { +#ifdef USE_ESP32_CAMERA + CameraImageRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str()); + this->on_camera_image_request(msg); +#endif + break; + } + case 48: { +#ifdef USE_CLIMATE + ClimateCommandRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str()); + this->on_climate_command_request(msg); +#endif + break; + } + default: + return false; + } + return true; +} + +void APIServerConnection::on_hello_request(const HelloRequest &msg) { + HelloResponse ret = this->hello(msg); + if (!this->send_hello_response(ret)) { + this->on_fatal_error(); + } +} +void APIServerConnection::on_connect_request(const ConnectRequest &msg) { + ConnectResponse ret = this->connect(msg); + if (!this->send_connect_response(ret)) { + this->on_fatal_error(); + } +} +void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { + DisconnectResponse ret = this->disconnect(msg); + if (!this->send_disconnect_response(ret)) { + this->on_fatal_error(); + } +} +void APIServerConnection::on_ping_request(const PingRequest &msg) { + PingResponse ret = this->ping(msg); + if (!this->send_ping_response(ret)) { + this->on_fatal_error(); + } +} +void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + DeviceInfoResponse ret = this->device_info(msg); + if (!this->send_device_info_response(ret)) { + this->on_fatal_error(); + } +} +void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->list_entities(msg); +} +void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->subscribe_states(msg); +} +void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->subscribe_logs(msg); +} +void APIServerConnection::on_subscribe_homeassistant_services_request( + const SubscribeHomeassistantServicesRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->subscribe_homeassistant_services(msg); +} +void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->subscribe_home_assistant_states(msg); +} +void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + GetTimeResponse ret = this->get_time(msg); + if (!this->send_get_time_response(ret)) { + this->on_fatal_error(); + } +} +void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->execute_service(msg); +} +#ifdef USE_COVER +void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->cover_command(msg); +} +#endif +#ifdef USE_FAN +void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->fan_command(msg); +} +#endif +#ifdef USE_LIGHT +void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->light_command(msg); +} +#endif +#ifdef USE_SWITCH +void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->switch_command(msg); +} +#endif +#ifdef USE_ESP32_CAMERA +void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->camera_image(msg); +} +#endif +#ifdef USE_CLIMATE +void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->climate_command(msg); +} +#endif + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h new file mode 100644 index 0000000000..16662811fe --- /dev/null +++ b/esphome/components/api/api_pb2_service.h @@ -0,0 +1,183 @@ +#pragma once + +#include "api_pb2.h" +#include "esphome/core/defines.h" + +namespace esphome { +namespace api { + +class APIServerConnectionBase : public ProtoService { + public: + virtual void on_hello_request(const HelloRequest &value){}; + bool send_hello_response(const HelloResponse &msg); + virtual void on_connect_request(const ConnectRequest &value){}; + bool send_connect_response(const ConnectResponse &msg); + bool send_disconnect_request(const DisconnectRequest &msg); + virtual void on_disconnect_request(const DisconnectRequest &value){}; + bool send_disconnect_response(const DisconnectResponse &msg); + virtual void on_disconnect_response(const DisconnectResponse &value){}; + bool send_ping_request(const PingRequest &msg); + virtual void on_ping_request(const PingRequest &value){}; + bool send_ping_response(const PingResponse &msg); + virtual void on_ping_response(const PingResponse &value){}; + virtual void on_device_info_request(const DeviceInfoRequest &value){}; + bool send_device_info_response(const DeviceInfoResponse &msg); + virtual void on_list_entities_request(const ListEntitiesRequest &value){}; + bool send_list_entities_done_response(const ListEntitiesDoneResponse &msg); + virtual void on_subscribe_states_request(const SubscribeStatesRequest &value){}; +#ifdef USE_BINARY_SENSOR + bool send_list_entities_binary_sensor_response(const ListEntitiesBinarySensorResponse &msg); +#endif +#ifdef USE_BINARY_SENSOR + bool send_binary_sensor_state_response(const BinarySensorStateResponse &msg); +#endif +#ifdef USE_COVER + bool send_list_entities_cover_response(const ListEntitiesCoverResponse &msg); +#endif +#ifdef USE_COVER + bool send_cover_state_response(const CoverStateResponse &msg); +#endif +#ifdef USE_COVER + virtual void on_cover_command_request(const CoverCommandRequest &value){}; +#endif +#ifdef USE_FAN + bool send_list_entities_fan_response(const ListEntitiesFanResponse &msg); +#endif +#ifdef USE_FAN + bool send_fan_state_response(const FanStateResponse &msg); +#endif +#ifdef USE_FAN + virtual void on_fan_command_request(const FanCommandRequest &value){}; +#endif +#ifdef USE_LIGHT + bool send_list_entities_light_response(const ListEntitiesLightResponse &msg); +#endif +#ifdef USE_LIGHT + bool send_light_state_response(const LightStateResponse &msg); +#endif +#ifdef USE_LIGHT + virtual void on_light_command_request(const LightCommandRequest &value){}; +#endif +#ifdef USE_SENSOR + bool send_list_entities_sensor_response(const ListEntitiesSensorResponse &msg); +#endif +#ifdef USE_SENSOR + bool send_sensor_state_response(const SensorStateResponse &msg); +#endif +#ifdef USE_SWITCH + bool send_list_entities_switch_response(const ListEntitiesSwitchResponse &msg); +#endif +#ifdef USE_SWITCH + bool send_switch_state_response(const SwitchStateResponse &msg); +#endif +#ifdef USE_SWITCH + virtual void on_switch_command_request(const SwitchCommandRequest &value){}; +#endif +#ifdef USE_TEXT_SENSOR + bool send_list_entities_text_sensor_response(const ListEntitiesTextSensorResponse &msg); +#endif +#ifdef USE_TEXT_SENSOR + bool send_text_sensor_state_response(const TextSensorStateResponse &msg); +#endif + virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; + bool send_subscribe_logs_response(const SubscribeLogsResponse &msg); + virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; + bool send_homeassistant_service_response(const HomeassistantServiceResponse &msg); + virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; + bool send_subscribe_home_assistant_state_response(const SubscribeHomeAssistantStateResponse &msg); + virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; + bool send_get_time_request(const GetTimeRequest &msg); + virtual void on_get_time_request(const GetTimeRequest &value){}; + bool send_get_time_response(const GetTimeResponse &msg); + virtual void on_get_time_response(const GetTimeResponse &value){}; + bool send_list_entities_services_response(const ListEntitiesServicesResponse &msg); + virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; +#ifdef USE_ESP32_CAMERA + bool send_list_entities_camera_response(const ListEntitiesCameraResponse &msg); +#endif +#ifdef USE_ESP32_CAMERA + bool send_camera_image_response(const CameraImageResponse &msg); +#endif +#ifdef USE_ESP32_CAMERA + virtual void on_camera_image_request(const CameraImageRequest &value){}; +#endif +#ifdef USE_CLIMATE + bool send_list_entities_climate_response(const ListEntitiesClimateResponse &msg); +#endif +#ifdef USE_CLIMATE + bool send_climate_state_response(const ClimateStateResponse &msg); +#endif +#ifdef USE_CLIMATE + virtual void on_climate_command_request(const ClimateCommandRequest &value){}; +#endif + protected: + bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; +}; + +class APIServerConnection : public APIServerConnectionBase { + public: + virtual HelloResponse hello(const HelloRequest &msg) = 0; + virtual ConnectResponse connect(const ConnectRequest &msg) = 0; + virtual DisconnectResponse disconnect(const DisconnectRequest &msg) = 0; + virtual PingResponse ping(const PingRequest &msg) = 0; + virtual DeviceInfoResponse device_info(const DeviceInfoRequest &msg) = 0; + virtual void list_entities(const ListEntitiesRequest &msg) = 0; + virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0; + virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; + virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; + virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; + virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; + virtual void execute_service(const ExecuteServiceRequest &msg) = 0; +#ifdef USE_COVER + virtual void cover_command(const CoverCommandRequest &msg) = 0; +#endif +#ifdef USE_FAN + virtual void fan_command(const FanCommandRequest &msg) = 0; +#endif +#ifdef USE_LIGHT + virtual void light_command(const LightCommandRequest &msg) = 0; +#endif +#ifdef USE_SWITCH + virtual void switch_command(const SwitchCommandRequest &msg) = 0; +#endif +#ifdef USE_ESP32_CAMERA + virtual void camera_image(const CameraImageRequest &msg) = 0; +#endif +#ifdef USE_CLIMATE + virtual void climate_command(const ClimateCommandRequest &msg) = 0; +#endif + protected: + void on_hello_request(const HelloRequest &msg) override; + void on_connect_request(const ConnectRequest &msg) override; + void on_disconnect_request(const DisconnectRequest &msg) override; + void on_ping_request(const PingRequest &msg) override; + void on_device_info_request(const DeviceInfoRequest &msg) override; + void on_list_entities_request(const ListEntitiesRequest &msg) override; + void on_subscribe_states_request(const SubscribeStatesRequest &msg) override; + void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; + void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; + void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; + void on_get_time_request(const GetTimeRequest &msg) override; + void on_execute_service_request(const ExecuteServiceRequest &msg) override; +#ifdef USE_COVER + void on_cover_command_request(const CoverCommandRequest &msg) override; +#endif +#ifdef USE_FAN + void on_fan_command_request(const FanCommandRequest &msg) override; +#endif +#ifdef USE_LIGHT + void on_light_command_request(const LightCommandRequest &msg) override; +#endif +#ifdef USE_SWITCH + void on_switch_command_request(const SwitchCommandRequest &msg) override; +#endif +#ifdef USE_ESP32_CAMERA + void on_camera_image_request(const CameraImageRequest &msg) override; +#endif +#ifdef USE_CLIMATE + void on_climate_command_request(const ClimateCommandRequest &msg) override; +#endif +}; + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index cde6f1e875..25ae9a98a3 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -1,19 +1,11 @@ -#include <utility> - #include "api_server.h" -#include "basic_messages.h" +#include "api_connection.h" #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/util.h" #include "esphome/core/defines.h" #include "esphome/core/version.h" -#ifdef USE_DEEP_SLEEP -#include "esphome/components/deep_sleep/deep_sleep_component.h" -#endif -#ifdef USE_HOMEASSISTANT_TIME -#include "esphome/components/homeassistant/time/homeassistant_time.h" -#endif #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -210,9 +202,9 @@ void APIServer::set_port(uint16_t port) { this->port_ = port; } APIServer *global_api_server = nullptr; void APIServer::set_password(const std::string &password) { this->password_ = password; } -void APIServer::send_service_call(ServiceCallResponse &call) { +void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { for (auto *client : this->clients_) { - client->send_service_call(call); + client->send_homeassistant_service_call(call); } } APIServer::APIServer() { global_api_server = this; } @@ -238,965 +230,10 @@ void APIServer::request_time() { bool APIServer::is_connected() const { return !this->clients_.empty(); } void APIServer::on_shutdown() { for (auto *c : this->clients_) { - c->send_disconnect_request(); + c->send_disconnect_request(DisconnectRequest()); } delay(10); } -// APIConnection -APIConnection::APIConnection(AsyncClient *client, APIServer *parent) - : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { - this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this); - this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this); - this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); }, - this); - this->client_->onData([](void *s, AsyncClient *c, void *buf, - size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast<uint8_t *>(buf), len); }, - this); - - this->send_buffer_.reserve(64); - this->recv_buffer_.reserve(32); - this->client_info_ = this->client_->remoteIP().toString().c_str(); - this->last_traffic_ = millis(); -} -APIConnection::~APIConnection() { delete this->client_; } -void APIConnection::on_error_(int8_t error) { - // disconnect will also be called, nothing to do here - this->remove_ = true; -} -void APIConnection::on_disconnect_() { - // delete self, generally unsafe but not in this case. - this->remove_ = true; -} -void APIConnection::on_timeout_(uint32_t time) { this->disconnect_client(); } -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); - // TODO: On ESP32, use queue to notify main thread of new data -} -void APIConnection::parse_recv_buffer_() { - if (this->recv_buffer_.empty() || this->remove_) - return; - - while (!this->recv_buffer_.empty()) { - if (this->recv_buffer_[0] != 0x00) { - ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str()); - this->fatal_error_(); - return; - } - uint32_t i = 1; - const uint32_t size = this->recv_buffer_.size(); - uint32_t msg_size = 0; - while (i < size) { - const uint8_t dat = this->recv_buffer_[i]; - msg_size |= (dat & 0x7F); - // consume - i += 1; - if ((dat & 0x80) == 0x00) { - break; - } else { - msg_size <<= 7; - } - } - if (i == size) - // not enough data there yet - return; - - uint32_t msg_type = 0; - bool msg_type_done = false; - while (i < size) { - const uint8_t dat = this->recv_buffer_[i]; - msg_type |= (dat & 0x7F); - // consume - i += 1; - if ((dat & 0x80) == 0x00) { - msg_type_done = true; - break; - } else { - msg_type <<= 7; - } - } - if (!msg_type_done) - // not enough data there yet - return; - - if (size - i < msg_size) - // message body not fully received - return; - - // ESP_LOGVV(TAG, "RECV Message: Size=%u Type=%u", msg_size, msg_type); - - if (!this->valid_rx_message_type_(msg_type)) { - ESP_LOGE(TAG, "Not a valid message type: %u", msg_type); - this->fatal_error_(); - 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); - } -} -void APIConnection::read_message_(uint32_t size, uint32_t type, uint8_t *msg) { - this->last_traffic_ = millis(); - - switch (static_cast<APIMessageType>(type)) { - case APIMessageType::HELLO_REQUEST: { - HelloRequest req; - req.decode(msg, size); - this->on_hello_request_(req); - break; - } - case APIMessageType::HELLO_RESPONSE: { - // Invalid - break; - } - case APIMessageType::CONNECT_REQUEST: { - ConnectRequest req; - req.decode(msg, size); - this->on_connect_request_(req); - break; - } - case APIMessageType::CONNECT_RESPONSE: - // Invalid - break; - case APIMessageType::DISCONNECT_REQUEST: { - DisconnectRequest req; - req.decode(msg, size); - this->on_disconnect_request_(req); - break; - } - case APIMessageType::DISCONNECT_RESPONSE: { - DisconnectResponse req; - req.decode(msg, size); - this->on_disconnect_response_(req); - break; - } - case APIMessageType::PING_REQUEST: { - PingRequest req; - req.decode(msg, size); - this->on_ping_request_(req); - break; - } - case APIMessageType::PING_RESPONSE: { - PingResponse req; - req.decode(msg, size); - this->on_ping_response_(req); - break; - } - case APIMessageType::DEVICE_INFO_REQUEST: { - DeviceInfoRequest req; - req.decode(msg, size); - this->on_device_info_request_(req); - break; - } - case APIMessageType::DEVICE_INFO_RESPONSE: { - // Invalid - break; - } - case APIMessageType::LIST_ENTITIES_REQUEST: { - ListEntitiesRequest req; - req.decode(msg, size); - this->on_list_entities_request_(req); - break; - } - case APIMessageType::LIST_ENTITIES_BINARY_SENSOR_RESPONSE: - case APIMessageType::LIST_ENTITIES_COVER_RESPONSE: - case APIMessageType::LIST_ENTITIES_FAN_RESPONSE: - case APIMessageType::LIST_ENTITIES_LIGHT_RESPONSE: - case APIMessageType::LIST_ENTITIES_SENSOR_RESPONSE: - case APIMessageType::LIST_ENTITIES_SWITCH_RESPONSE: - case APIMessageType::LIST_ENTITIES_TEXT_SENSOR_RESPONSE: - case APIMessageType::LIST_ENTITIES_SERVICE_RESPONSE: - case APIMessageType::LIST_ENTITIES_CAMERA_RESPONSE: - case APIMessageType::LIST_ENTITIES_CLIMATE_RESPONSE: - case APIMessageType::LIST_ENTITIES_DONE_RESPONSE: - // Invalid - break; - case APIMessageType::SUBSCRIBE_STATES_REQUEST: { - SubscribeStatesRequest req; - req.decode(msg, size); - this->on_subscribe_states_request_(req); - break; - } - case APIMessageType::BINARY_SENSOR_STATE_RESPONSE: - case APIMessageType::COVER_STATE_RESPONSE: - case APIMessageType::FAN_STATE_RESPONSE: - case APIMessageType::LIGHT_STATE_RESPONSE: - case APIMessageType::SENSOR_STATE_RESPONSE: - case APIMessageType::SWITCH_STATE_RESPONSE: - case APIMessageType::TEXT_SENSOR_STATE_RESPONSE: - case APIMessageType::CAMERA_IMAGE_RESPONSE: - case APIMessageType::CLIMATE_STATE_RESPONSE: - // Invalid - break; - case APIMessageType::SUBSCRIBE_LOGS_REQUEST: { - SubscribeLogsRequest req; - req.decode(msg, size); - this->on_subscribe_logs_request_(req); - break; - } - case APIMessageType ::SUBSCRIBE_LOGS_RESPONSE: - // Invalid - break; - case APIMessageType::COVER_COMMAND_REQUEST: { -#ifdef USE_COVER - CoverCommandRequest req; - req.decode(msg, size); - this->on_cover_command_request_(req); -#endif - break; - } - case APIMessageType::FAN_COMMAND_REQUEST: { -#ifdef USE_FAN - FanCommandRequest req; - req.decode(msg, size); - this->on_fan_command_request_(req); -#endif - break; - } - case APIMessageType::LIGHT_COMMAND_REQUEST: { -#ifdef USE_LIGHT - LightCommandRequest req; - req.decode(msg, size); - this->on_light_command_request_(req); -#endif - break; - } - case APIMessageType::SWITCH_COMMAND_REQUEST: { -#ifdef USE_SWITCH - SwitchCommandRequest req; - req.decode(msg, size); - this->on_switch_command_request_(req); -#endif - break; - } - case APIMessageType::CLIMATE_COMMAND_REQUEST: { -#ifdef USE_CLIMATE - ClimateCommandRequest req; - req.decode(msg, size); - this->on_climate_command_request_(req); -#endif - break; - } - case APIMessageType::SUBSCRIBE_SERVICE_CALLS_REQUEST: { - SubscribeServiceCallsRequest req; - req.decode(msg, size); - this->on_subscribe_service_calls_request_(req); - break; - } - case APIMessageType::SERVICE_CALL_RESPONSE: - // Invalid - break; - case APIMessageType::GET_TIME_REQUEST: - // Invalid - break; - case APIMessageType::GET_TIME_RESPONSE: { -#ifdef USE_HOMEASSISTANT_TIME - homeassistant::GetTimeResponse req; - req.decode(msg, size); -#endif - break; - } - case APIMessageType::SUBSCRIBE_HOME_ASSISTANT_STATES_REQUEST: { - SubscribeHomeAssistantStatesRequest req; - req.decode(msg, size); - this->on_subscribe_home_assistant_states_request_(req); - break; - } - case APIMessageType::SUBSCRIBE_HOME_ASSISTANT_STATE_RESPONSE: - // Invalid - break; - case APIMessageType::HOME_ASSISTANT_STATE_RESPONSE: { - HomeAssistantStateResponse req; - req.decode(msg, size); - this->on_home_assistant_state_response_(req); - break; - } - case APIMessageType::EXECUTE_SERVICE_REQUEST: { - ExecuteServiceRequest req; - req.decode(msg, size); - this->on_execute_service_(req); - break; - } - case APIMessageType::CAMERA_IMAGE_REQUEST: { -#ifdef USE_ESP32_CAMERA - CameraImageRequest req; - req.decode(msg, size); - this->on_camera_image_request_(req); -#endif - break; - } - } -} -void APIConnection::on_hello_request_(const HelloRequest &req) { - ESP_LOGVV(TAG, "on_hello_request_(client_info='%s')", req.get_client_info().c_str()); - this->client_info_ = req.get_client_info() + " (" + this->client_->remoteIP().toString().c_str(); - this->client_info_ += ")"; - ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str()); - - auto buffer = this->get_buffer(); - // uint32 api_version_major = 1; -> 1 - buffer.encode_uint32(1, 1); - // uint32 api_version_minor = 2; -> 1 - buffer.encode_uint32(2, 1); - - // string server_info = 3; - buffer.encode_string(3, App.get_name() + " (esphome v" ESPHOME_VERSION ")"); - bool success = this->send_buffer(APIMessageType::HELLO_RESPONSE); - if (!success) { - this->fatal_error_(); - return; - } - - this->connection_state_ = ConnectionState::WAITING_FOR_CONNECT; -} -void APIConnection::on_connect_request_(const ConnectRequest &req) { - ESP_LOGVV(TAG, "on_connect_request_(password='%s')", req.get_password().c_str()); - bool correct = this->parent_->check_password(req.get_password()); - auto buffer = this->get_buffer(); - // bool invalid_password = 1; - buffer.encode_bool(1, !correct); - bool success = this->send_buffer(APIMessageType::CONNECT_RESPONSE); - if (!success) { - this->fatal_error_(); - return; - } - - if (correct) { - ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str()); - this->connection_state_ = ConnectionState::CONNECTED; - -#ifdef USE_HOMEASSISTANT_TIME - if (homeassistant::global_homeassistant_time != nullptr) { - this->send_time_request(); - } -#endif - } -} -void APIConnection::on_disconnect_request_(const DisconnectRequest &req) { - ESP_LOGVV(TAG, "on_disconnect_request_"); - // remote initiated disconnect_client - if (!this->send_empty_message(APIMessageType::DISCONNECT_RESPONSE)) { - this->fatal_error_(); - return; - } - this->disconnect_client(); -} -void APIConnection::on_disconnect_response_(const DisconnectResponse &req) { - ESP_LOGVV(TAG, "on_disconnect_response_"); - // we initiated disconnect_client - this->disconnect_client(); -} -void APIConnection::on_ping_request_(const PingRequest &req) { - ESP_LOGVV(TAG, "on_ping_request_"); - PingResponse resp; - this->send_message(resp); -} -void APIConnection::on_ping_response_(const PingResponse &req) { - ESP_LOGVV(TAG, "on_ping_response_"); - // we initiated ping - this->sent_ping_ = false; -} -void APIConnection::on_device_info_request_(const DeviceInfoRequest &req) { - ESP_LOGVV(TAG, "on_device_info_request_"); - auto buffer = this->get_buffer(); - // bool uses_password = 1; - buffer.encode_bool(1, this->parent_->uses_password()); - // string name = 2; - buffer.encode_string(2, App.get_name()); - // string mac_address = 3; - buffer.encode_string(3, get_mac_address_pretty()); - // string esphome_version = 4; - buffer.encode_string(4, ESPHOME_VERSION); - // string compilation_time = 5; - buffer.encode_string(5, App.get_compilation_time()); -#ifdef ARDUINO_BOARD - // string model = 6; - buffer.encode_string(6, ARDUINO_BOARD); -#endif -#ifdef USE_DEEP_SLEEP - // bool has_deep_sleep = 7; - buffer.encode_bool(7, deep_sleep::global_has_deep_sleep); -#endif - this->send_buffer(APIMessageType::DEVICE_INFO_RESPONSE); -} -void APIConnection::on_list_entities_request_(const ListEntitiesRequest &req) { - ESP_LOGVV(TAG, "on_list_entities_request_"); - this->list_entities_iterator_.begin(); -} -void APIConnection::on_subscribe_states_request_(const SubscribeStatesRequest &req) { - ESP_LOGVV(TAG, "on_subscribe_states_request_"); - this->state_subscription_ = true; - this->initial_state_iterator_.begin(); -} -void APIConnection::on_subscribe_logs_request_(const SubscribeLogsRequest &req) { - ESP_LOGVV(TAG, "on_subscribe_logs_request_"); - this->log_subscription_ = req.get_level(); - if (req.get_dump_config()) { - App.schedule_dump_config(); - } -} - -void APIConnection::fatal_error_() { - this->client_->close(); - this->remove_ = true; -} -bool APIConnection::valid_rx_message_type_(uint32_t type) { - switch (static_cast<APIMessageType>(type)) { - case APIMessageType::HELLO_RESPONSE: - case APIMessageType::CONNECT_RESPONSE: - return false; - case APIMessageType::HELLO_REQUEST: - return this->connection_state_ == ConnectionState::WAITING_FOR_HELLO; - case APIMessageType::CONNECT_REQUEST: - return this->connection_state_ == ConnectionState::WAITING_FOR_CONNECT; - case APIMessageType::PING_REQUEST: - case APIMessageType::PING_RESPONSE: - case APIMessageType::DISCONNECT_REQUEST: - case APIMessageType::DISCONNECT_RESPONSE: - case APIMessageType::DEVICE_INFO_REQUEST: - if (this->connection_state_ == ConnectionState::WAITING_FOR_CONNECT) - return true; - default: - return this->connection_state_ == ConnectionState::CONNECTED; - } -} -bool APIConnection::send_message(APIMessage &msg) { - this->send_buffer_.clear(); - APIBuffer buf(&this->send_buffer_); - msg.encode(buf); - return this->send_buffer(msg.message_type()); -} -bool APIConnection::send_empty_message(APIMessageType type) { - this->send_buffer_.clear(); - return this->send_buffer(type); -} - -void APIConnection::disconnect_client() { - this->client_->close(); - this->remove_ = true; -} -void encode_varint(uint8_t *dat, uint8_t *len, uint32_t value) { - if (value <= 0x7F) { - *dat = value; - (*len)++; - return; - } - - while (value) { - uint8_t temp = value & 0x7F; - value >>= 7; - if (value) { - *dat = temp | 0x80; - } else { - *dat = temp; - } - dat++; - (*len)++; - } -} - -bool APIConnection::send_buffer(APIMessageType type) { - uint8_t header[20]; - header[0] = 0x00; - uint8_t header_len = 1; - encode_varint(header + header_len, &header_len, this->send_buffer_.size()); - encode_varint(header + header_len, &header_len, static_cast<uint32_t>(type)); - - size_t needed_space = this->send_buffer_.size() + header_len; - - if (needed_space > this->client_->space()) { - delay(0); - if (needed_space > this->client_->space()) { - if (type != APIMessageType::SUBSCRIBE_LOGS_RESPONSE) { - ESP_LOGV(TAG, "Cannot send message because of TCP buffer space"); - } - delay(0); - return false; - } - } - - // char buffer[512]; - // uint32_t offset = 0; - // for (int j = 0; j < header_len; j++) { - // offset += snprintf(buffer + offset, 512 - offset, "0x%02X ", header[j]); - // } - // offset += snprintf(buffer + offset, 512 - offset, "| "); - // for (auto &it : this->send_buffer_) { - // int i = snprintf(buffer + offset, 512 - offset, "0x%02X ", it); - // if (i <= 0) - // break; - // offset += i; - // } - // ESP_LOGVV(TAG, "SEND %s", buffer); - - this->client_->add(reinterpret_cast<char *>(header), header_len); - this->client_->add(reinterpret_cast<char *>(this->send_buffer_.data()), this->send_buffer_.size()); - return this->client_->send(); -} - -void APIConnection::loop() { - if (!network_is_connected()) { - // when network is disconnected force disconnect immediately - // don't wait for timeout - this->fatal_error_(); - return; - } - if (this->client_->disconnected()) { - // failsafe for disconnect logic - this->on_disconnect_(); - return; - } - this->parse_recv_buffer_(); - - this->list_entities_iterator_.advance(); - this->initial_state_iterator_.advance(); - - const uint32_t keepalive = 60000; - if (this->sent_ping_) { - if (millis() - this->last_traffic_ > (keepalive * 3) / 2) { - ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); - this->disconnect_client(); - } - } else if (millis() - this->last_traffic_ > keepalive) { - this->sent_ping_ = true; - this->send_ping_request(); - } - -#ifdef USE_ESP32_CAMERA - if (this->image_reader_.available()) { - uint32_t space = this->client_->space(); - // reserve 15 bytes for metadata, and at least 64 bytes of data - if (space >= 15 + 64) { - uint32_t to_send = std::min(space - 15, this->image_reader_.available()); - auto buffer = this->get_buffer(); - // fixed32 key = 1; - buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); - // bytes data = 2; - buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); - // bool done = 3; - bool done = this->image_reader_.available() == to_send; - buffer.encode_bool(3, done); - bool success = this->send_buffer(APIMessageType::CAMERA_IMAGE_RESPONSE); - if (success) { - this->image_reader_.consume_data(to_send); - } - if (success && done) { - this->image_reader_.return_image(); - } - } - } -#endif -} - -#ifdef USE_BINARY_SENSOR -bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) { - if (!this->state_subscription_) - return false; - - auto buffer = this->get_buffer(); - // fixed32 key = 1; - buffer.encode_fixed32(1, binary_sensor->get_object_id_hash()); - // bool state = 2; - buffer.encode_bool(2, state); - return this->send_buffer(APIMessageType::BINARY_SENSOR_STATE_RESPONSE); -} -#endif - -#ifdef USE_COVER -bool APIConnection::send_cover_state(cover::Cover *cover) { - if (!this->state_subscription_) - return false; - - auto buffer = this->get_buffer(); - auto traits = cover->get_traits(); - // fixed32 key = 1; - buffer.encode_fixed32(1, cover->get_object_id_hash()); - // enum LegacyCoverState { - // OPEN = 0; - // CLOSED = 1; - // } - // LegacyCoverState legacy_state = 2; - uint32_t state = (cover->position == cover::COVER_OPEN) ? 0 : 1; - buffer.encode_uint32(2, state); - // float position = 3; - buffer.encode_float(3, cover->position); - if (traits.get_supports_tilt()) { - // float tilt = 4; - buffer.encode_float(4, cover->tilt); - } - // enum CoverCurrentOperation { - // IDLE = 0; - // IS_OPENING = 1; - // IS_CLOSING = 2; - // } - // CoverCurrentOperation current_operation = 5; - buffer.encode_uint32(5, cover->current_operation); - return this->send_buffer(APIMessageType::COVER_STATE_RESPONSE); -} -#endif - -#ifdef USE_FAN -bool APIConnection::send_fan_state(fan::FanState *fan) { - if (!this->state_subscription_) - return false; - - auto buffer = this->get_buffer(); - // fixed32 key = 1; - buffer.encode_fixed32(1, fan->get_object_id_hash()); - // bool state = 2; - buffer.encode_bool(2, fan->state); - // bool oscillating = 3; - if (fan->get_traits().supports_oscillation()) { - buffer.encode_bool(3, fan->oscillating); - } - // enum FanSpeed { - // LOW = 0; - // MEDIUM = 1; - // HIGH = 2; - // } - // FanSpeed speed = 4; - if (fan->get_traits().supports_speed()) { - buffer.encode_uint32(4, fan->speed); - } - return this->send_buffer(APIMessageType::FAN_STATE_RESPONSE); -} -#endif - -#ifdef USE_LIGHT -bool APIConnection::send_light_state(light::LightState *light) { - if (!this->state_subscription_) - return false; - - auto buffer = this->get_buffer(); - auto traits = light->get_traits(); - auto values = light->remote_values; - - // fixed32 key = 1; - buffer.encode_fixed32(1, light->get_object_id_hash()); - // bool state = 2; - buffer.encode_bool(2, values.get_state() != 0.0f); - // float brightness = 3; - if (traits.get_supports_brightness()) { - buffer.encode_float(3, values.get_brightness()); - } - if (traits.get_supports_rgb()) { - // float red = 4; - buffer.encode_float(4, values.get_red()); - // float green = 5; - buffer.encode_float(5, values.get_green()); - // float blue = 6; - buffer.encode_float(6, values.get_blue()); - } - // float white = 7; - if (traits.get_supports_rgb_white_value()) { - buffer.encode_float(7, values.get_white()); - } - // float color_temperature = 8; - if (traits.get_supports_color_temperature()) { - buffer.encode_float(8, values.get_color_temperature()); - } - // string effect = 9; - if (light->supports_effects()) { - buffer.encode_string(9, light->get_effect_name()); - } - return this->send_buffer(APIMessageType::LIGHT_STATE_RESPONSE); -} -#endif - -#ifdef USE_SENSOR -bool APIConnection::send_sensor_state(sensor::Sensor *sensor, float state) { - if (!this->state_subscription_) - return false; - - auto buffer = this->get_buffer(); - // fixed32 key = 1; - buffer.encode_fixed32(1, sensor->get_object_id_hash()); - // float state = 2; - buffer.encode_float(2, state); - return this->send_buffer(APIMessageType::SENSOR_STATE_RESPONSE); -} -#endif - -#ifdef USE_SWITCH -bool APIConnection::send_switch_state(switch_::Switch *a_switch, bool state) { - if (!this->state_subscription_) - return false; - - auto buffer = this->get_buffer(); - // fixed32 key = 1; - buffer.encode_fixed32(1, a_switch->get_object_id_hash()); - // bool state = 2; - buffer.encode_bool(2, state); - return this->send_buffer(APIMessageType::SWITCH_STATE_RESPONSE); -} -#endif - -#ifdef USE_TEXT_SENSOR -bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state) { - if (!this->state_subscription_) - return false; - - auto buffer = this->get_buffer(); - // fixed32 key = 1; - buffer.encode_fixed32(1, text_sensor->get_object_id_hash()); - // string state = 2; - buffer.encode_string(2, state); - return this->send_buffer(APIMessageType::TEXT_SENSOR_STATE_RESPONSE); -} -#endif - -#ifdef USE_CLIMATE -bool APIConnection::send_climate_state(climate::Climate *climate) { - if (!this->state_subscription_) - return false; - - auto buffer = this->get_buffer(); - auto traits = climate->get_traits(); - // fixed32 key = 1; - buffer.encode_fixed32(1, climate->get_object_id_hash()); - // ClimateMode mode = 2; - buffer.encode_uint32(2, static_cast<uint32_t>(climate->mode)); - // float current_temperature = 3; - if (traits.get_supports_current_temperature()) { - buffer.encode_float(3, climate->current_temperature); - } - if (traits.get_supports_two_point_target_temperature()) { - // float target_temperature_low = 5; - buffer.encode_float(5, climate->target_temperature_low); - // float target_temperature_high = 6; - buffer.encode_float(6, climate->target_temperature_high); - } else { - // float target_temperature = 4; - buffer.encode_float(4, climate->target_temperature); - } - // bool away = 7; - if (traits.get_supports_away()) { - buffer.encode_bool(7, climate->away); - } - return this->send_buffer(APIMessageType::CLIMATE_STATE_RESPONSE); -} -#endif - -bool APIConnection::send_log_message(int level, const char *tag, const char *line) { - if (this->log_subscription_ < level) - return false; - - auto buffer = this->get_buffer(); - // LogLevel level = 1; - buffer.encode_uint32(1, static_cast<uint32_t>(level)); - // string tag = 2; - // buffer.encode_string(2, tag, strlen(tag)); - // string message = 3; - buffer.encode_string(3, line, strlen(line)); - bool success = this->send_buffer(APIMessageType::SUBSCRIBE_LOGS_RESPONSE); - - if (!success) { - buffer = this->get_buffer(); - // bool send_failed = 4; - buffer.encode_bool(4, true); - return this->send_buffer(APIMessageType::SUBSCRIBE_LOGS_RESPONSE); - } else { - return true; - } -} -bool APIConnection::send_disconnect_request() { - DisconnectRequest req; - return this->send_message(req); -} -bool APIConnection::send_ping_request() { - ESP_LOGVV(TAG, "Sending ping..."); - PingRequest req; - return this->send_message(req); -} - -#ifdef USE_COVER -void APIConnection::on_cover_command_request_(const CoverCommandRequest &req) { - ESP_LOGVV(TAG, "on_cover_command_request_"); - cover::Cover *cover = App.get_cover_by_key(req.get_key()); - if (cover == nullptr) - return; - - auto call = cover->make_call(); - if (req.get_legacy_command().has_value()) { - auto cmd = *req.get_legacy_command(); - switch (cmd) { - case LEGACY_COVER_COMMAND_OPEN: - call.set_command_open(); - break; - case LEGACY_COVER_COMMAND_CLOSE: - call.set_command_close(); - break; - case LEGACY_COVER_COMMAND_STOP: - call.set_command_stop(); - break; - } - } - if (req.get_position().has_value()) { - auto pos = *req.get_position(); - call.set_position(pos); - } - if (req.get_tilt().has_value()) { - auto tilt = *req.get_tilt(); - call.set_tilt(tilt); - } - if (req.get_stop()) { - call.set_command_stop(); - } - call.perform(); -} -#endif - -#ifdef USE_FAN -void APIConnection::on_fan_command_request_(const FanCommandRequest &req) { - ESP_LOGVV(TAG, "on_fan_command_request_"); - fan::FanState *fan = App.get_fan_by_key(req.get_key()); - if (fan == nullptr) - return; - - auto call = fan->make_call(); - call.set_state(req.get_state()); - call.set_oscillating(req.get_oscillating()); - call.set_speed(req.get_speed()); - call.perform(); -} -#endif - -#ifdef USE_LIGHT -void APIConnection::on_light_command_request_(const LightCommandRequest &req) { - ESP_LOGVV(TAG, "on_light_command_request_"); - light::LightState *light = App.get_light_by_key(req.get_key()); - if (light == nullptr) - return; - - auto call = light->make_call(); - call.set_state(req.get_state()); - call.set_brightness(req.get_brightness()); - call.set_red(req.get_red()); - call.set_green(req.get_green()); - call.set_blue(req.get_blue()); - call.set_white(req.get_white()); - call.set_color_temperature(req.get_color_temperature()); - call.set_transition_length(req.get_transition_length()); - call.set_flash_length(req.get_flash_length()); - call.set_effect(req.get_effect()); - call.perform(); -} -#endif - -#ifdef USE_SWITCH -void APIConnection::on_switch_command_request_(const SwitchCommandRequest &req) { - ESP_LOGVV(TAG, "on_switch_command_request_"); - switch_::Switch *a_switch = App.get_switch_by_key(req.get_key()); - if (a_switch == nullptr || a_switch->is_internal()) - return; - - if (req.get_state()) { - a_switch->turn_on(); - } else { - a_switch->turn_off(); - } -} -#endif - -#ifdef USE_CLIMATE -void APIConnection::on_climate_command_request_(const ClimateCommandRequest &req) { - ESP_LOGVV(TAG, "on_climate_command_request_"); - climate::Climate *climate = App.get_climate_by_key(req.get_key()); - if (climate == nullptr) - return; - - auto call = climate->make_call(); - if (req.get_mode().has_value()) - call.set_mode(*req.get_mode()); - if (req.get_target_temperature().has_value()) - call.set_target_temperature(*req.get_target_temperature()); - if (req.get_target_temperature_low().has_value()) - call.set_target_temperature_low(*req.get_target_temperature_low()); - if (req.get_target_temperature_high().has_value()) - call.set_target_temperature_high(*req.get_target_temperature_high()); - if (req.get_away().has_value()) - call.set_away(*req.get_away()); - call.perform(); -} -#endif - -void APIConnection::on_subscribe_service_calls_request_(const SubscribeServiceCallsRequest &req) { - this->service_call_subscription_ = true; -} -void APIConnection::send_service_call(ServiceCallResponse &call) { - if (!this->service_call_subscription_) - return; - - this->send_message(call); -} -void APIConnection::on_subscribe_home_assistant_states_request_(const SubscribeHomeAssistantStatesRequest &req) { - for (auto &it : this->parent_->get_state_subs()) { - auto buffer = this->get_buffer(); - // string entity_id = 1; - buffer.encode_string(1, it.entity_id); - this->send_buffer(APIMessageType::SUBSCRIBE_HOME_ASSISTANT_STATE_RESPONSE); - } -} -void APIConnection::on_home_assistant_state_response_(const HomeAssistantStateResponse &req) { - for (auto &it : this->parent_->get_state_subs()) { - if (it.entity_id == req.get_entity_id()) { - it.callback(req.get_state()); - } - } -} -void APIConnection::on_execute_service_(const ExecuteServiceRequest &req) { - ESP_LOGVV(TAG, "on_execute_service_"); - bool found = false; - for (auto *service : this->parent_->get_user_services()) { - if (service->execute_service(req)) { - found = true; - } - } - if (!found) { - ESP_LOGV(TAG, "Could not find matching service!"); - } -} - -APIBuffer APIConnection::get_buffer() { - this->send_buffer_.clear(); - return {&this->send_buffer_}; -} -#ifdef USE_HOMEASSISTANT_TIME -void APIConnection::send_time_request() { this->send_empty_message(APIMessageType::GET_TIME_REQUEST); } -#endif - -#ifdef USE_ESP32_CAMERA -void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) { - if (!this->state_subscription_) - return; - if (this->image_reader_.available()) - return; - this->image_reader_.set_image(image); -} -#endif - -#ifdef USE_ESP32_CAMERA -void APIConnection::on_camera_image_request_(const CameraImageRequest &req) { - if (esp32_camera::global_esp32_camera == nullptr) - return; - - ESP_LOGV(TAG, "on_camera_image_request_ stream=%s single=%s", YESNO(req.get_stream()), YESNO(req.get_single())); - if (req.get_single()) { - esp32_camera::global_esp32_camera->request_image(); - } - if (req.get_stream()) { - esp32_camera::global_esp32_camera->request_stream(); - } -} -#endif - } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index be75d87264..db826c55c2 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -4,14 +4,12 @@ #include "esphome/core/controller.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" +#include "api_pb2.h" +#include "api_pb2_service.h" #include "util.h" -#include "api_message.h" -#include "basic_messages.h" #include "list_entities.h" #include "subscribe_state.h" -#include "subscribe_logs.h" -#include "command_messages.h" -#include "service_call_message.h" +#include "homeassistant_service.h" #include "user_services.h" #ifdef ARDUINO_ARCH_ESP32 @@ -24,130 +22,6 @@ namespace esphome { namespace api { -class APIServer; - -class APIConnection { - public: - APIConnection(AsyncClient *client, APIServer *parent); - ~APIConnection(); - - void disconnect_client(); - APIBuffer get_buffer(); - bool send_buffer(APIMessageType type); - bool send_message(APIMessage &msg); - bool send_empty_message(APIMessageType type); - void loop(); - -#ifdef USE_BINARY_SENSOR - bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state); -#endif -#ifdef USE_COVER - bool send_cover_state(cover::Cover *cover); -#endif -#ifdef USE_FAN - bool send_fan_state(fan::FanState *fan); -#endif -#ifdef USE_LIGHT - bool send_light_state(light::LightState *light); -#endif -#ifdef USE_SENSOR - bool send_sensor_state(sensor::Sensor *sensor, float state); -#endif -#ifdef USE_SWITCH - bool send_switch_state(switch_::Switch *a_switch, bool state); -#endif -#ifdef USE_TEXT_SENSOR - bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state); -#endif -#ifdef USE_ESP32_CAMERA - void send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image); -#endif -#ifdef USE_CLIMATE - bool send_climate_state(climate::Climate *climate); -#endif - bool send_log_message(int level, const char *tag, const char *line); - bool send_disconnect_request(); - bool send_ping_request(); - void send_service_call(ServiceCallResponse &call); -#ifdef USE_HOMEASSISTANT_TIME - void send_time_request(); -#endif - - protected: - friend APIServer; - - void on_error_(int8_t error); - void on_disconnect_(); - void on_timeout_(uint32_t time); - void on_data_(uint8_t *buf, size_t len); - void fatal_error_(); - bool valid_rx_message_type_(uint32_t msg_type); - void read_message_(uint32_t size, uint32_t type, uint8_t *msg); - void parse_recv_buffer_(); - - // request types - void on_hello_request_(const HelloRequest &req); - void on_connect_request_(const ConnectRequest &req); - void on_disconnect_request_(const DisconnectRequest &req); - void on_disconnect_response_(const DisconnectResponse &req); - void on_ping_request_(const PingRequest &req); - void on_ping_response_(const PingResponse &req); - void on_device_info_request_(const DeviceInfoRequest &req); - void on_list_entities_request_(const ListEntitiesRequest &req); - void on_subscribe_states_request_(const SubscribeStatesRequest &req); - void on_subscribe_logs_request_(const SubscribeLogsRequest &req); -#ifdef USE_COVER - void on_cover_command_request_(const CoverCommandRequest &req); -#endif -#ifdef USE_FAN - void on_fan_command_request_(const FanCommandRequest &req); -#endif -#ifdef USE_LIGHT - void on_light_command_request_(const LightCommandRequest &req); -#endif -#ifdef USE_SWITCH - void on_switch_command_request_(const SwitchCommandRequest &req); -#endif -#ifdef USE_CLIMATE - void on_climate_command_request_(const ClimateCommandRequest &req); -#endif - void on_subscribe_service_calls_request_(const SubscribeServiceCallsRequest &req); - void on_subscribe_home_assistant_states_request_(const SubscribeHomeAssistantStatesRequest &req); - void on_home_assistant_state_response_(const HomeAssistantStateResponse &req); - void on_execute_service_(const ExecuteServiceRequest &req); -#ifdef USE_ESP32_CAMERA - void on_camera_image_request_(const CameraImageRequest &req); -#endif - - enum class ConnectionState { - WAITING_FOR_HELLO, - WAITING_FOR_CONNECT, - CONNECTED, - } connection_state_{ConnectionState::WAITING_FOR_HELLO}; - - bool remove_{false}; - - std::vector<uint8_t> send_buffer_; - std::vector<uint8_t> recv_buffer_; - - std::string client_info_; -#ifdef USE_ESP32_CAMERA - esp32_camera::CameraImageReader image_reader_; -#endif - - bool state_subscription_{false}; - int log_subscription_{ESPHOME_LOG_LEVEL_NONE}; - uint32_t last_traffic_; - bool sent_ping_{false}; - bool service_call_subscription_{false}; - AsyncClient *client_; - APIServer *parent_; - InitialStateIterator initial_state_iterator_; - ListEntitiesIterator list_entities_iterator_; -}; - -template<typename... Ts> class HomeAssistantServiceCallAction; - class APIServer : public Component, public Controller { public: APIServer(); @@ -187,7 +61,7 @@ class APIServer : public Component, public Controller { #ifdef USE_CLIMATE void on_climate_update(climate::Climate *obj) override; #endif - void send_service_call(ServiceCallResponse &call); + void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #ifdef USE_HOMEASSISTANT_TIME void request_time(); @@ -217,22 +91,6 @@ class APIServer : public Component, public Controller { extern APIServer *global_api_server; -template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> { - public: - explicit HomeAssistantServiceCallAction(APIServer *parent) : parent_(parent) {} - void set_service(const std::string &service) { this->resp_.set_service(service); } - void set_data(const std::vector<KeyValuePair> &data) { this->resp_.set_data(data); } - void set_data_template(const std::vector<KeyValuePair> &data_template) { - this->resp_.set_data_template(data_template); - } - void set_variables(const std::vector<TemplatableKeyValuePair> &variables) { this->resp_.set_variables(variables); } - void play(Ts... x) override { this->parent_->send_service_call(this->resp_); } - - protected: - APIServer *parent_; - ServiceCallResponse resp_; -}; - template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> { public: bool check(Ts... x) override { return global_api_server->is_connected(); } diff --git a/esphome/components/api/basic_messages.cpp b/esphome/components/api/basic_messages.cpp deleted file mode 100644 index 71e0045c05..0000000000 --- a/esphome/components/api/basic_messages.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "basic_messages.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace api { - -// Hello -bool HelloRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) { - switch (field_id) { - case 1: // string client_info = 1; - this->client_info_ = as_string(value, len); - return true; - default: - return false; - } -} -const std::string &HelloRequest::get_client_info() const { return this->client_info_; } -void HelloRequest::set_client_info(const std::string &client_info) { this->client_info_ = client_info; } -APIMessageType HelloRequest::message_type() const { return APIMessageType::HELLO_REQUEST; } - -// Connect -bool ConnectRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) { - switch (field_id) { - case 1: // string password = 1; - this->password_ = as_string(value, len); - return true; - default: - return false; - } -} -const std::string &ConnectRequest::get_password() const { return this->password_; } -void ConnectRequest::set_password(const std::string &password) { this->password_ = password; } -APIMessageType ConnectRequest::message_type() const { return APIMessageType::CONNECT_REQUEST; } - -APIMessageType DeviceInfoRequest::message_type() const { return APIMessageType::DEVICE_INFO_REQUEST; } -APIMessageType DisconnectRequest::message_type() const { return APIMessageType::DISCONNECT_REQUEST; } -bool DisconnectRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) { - switch (field_id) { - case 1: // string reason = 1; - this->reason_ = as_string(value, len); - return true; - default: - return false; - } -} -const std::string &DisconnectRequest::get_reason() const { return this->reason_; } -void DisconnectRequest::set_reason(const std::string &reason) { this->reason_ = reason; } -void DisconnectRequest::encode(APIBuffer &buffer) { - // string reason = 1; - buffer.encode_string(1, this->reason_); -} -APIMessageType DisconnectResponse::message_type() const { return APIMessageType::DISCONNECT_RESPONSE; } -APIMessageType PingRequest::message_type() const { return APIMessageType::PING_REQUEST; } -APIMessageType PingResponse::message_type() const { return APIMessageType::PING_RESPONSE; } - -} // namespace api -} // namespace esphome diff --git a/esphome/components/api/basic_messages.h b/esphome/components/api/basic_messages.h deleted file mode 100644 index 213a053514..0000000000 --- a/esphome/components/api/basic_messages.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include "api_message.h" - -namespace esphome { -namespace api { - -class HelloRequest : public APIMessage { - public: - bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override; - const std::string &get_client_info() const; - void set_client_info(const std::string &client_info); - APIMessageType message_type() const override; - - protected: - std::string client_info_; -}; - -class ConnectRequest : public APIMessage { - public: - bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override; - const std::string &get_password() const; - void set_password(const std::string &password); - APIMessageType message_type() const override; - - protected: - std::string password_; -}; - -class DeviceInfoRequest : public APIMessage { - public: - APIMessageType message_type() const override; -}; - -class DisconnectRequest : public APIMessage { - public: - bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override; - void encode(APIBuffer &buffer) override; - APIMessageType message_type() const override; - const std::string &get_reason() const; - void set_reason(const std::string &reason); - - protected: - std::string reason_; -}; - -class DisconnectResponse : public APIMessage { - public: - APIMessageType message_type() const override; -}; - -class PingRequest : public APIMessage { - public: - APIMessageType message_type() const override; -}; - -class PingResponse : public APIMessage { - public: - APIMessageType message_type() const override; -}; - -} // namespace api -} // namespace esphome diff --git a/esphome/components/api/command_messages.cpp b/esphome/components/api/command_messages.cpp deleted file mode 100644 index 240bc05cb7..0000000000 --- a/esphome/components/api/command_messages.cpp +++ /dev/null @@ -1,417 +0,0 @@ -#include "command_messages.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace api { - -#ifdef USE_COVER -bool CoverCommandRequest::decode_varint(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 2: - // bool has_legacy_command = 2; - this->has_legacy_command_ = value; - return true; - case 3: - // enum LegacyCoverCommand { - // OPEN = 0; - // CLOSE = 1; - // STOP = 2; - // } - // LegacyCoverCommand legacy_command_ = 3; - this->legacy_command_ = static_cast<LegacyCoverCommand>(value); - return true; - case 4: - // bool has_position = 4; - this->has_position_ = value; - return true; - case 6: - // bool has_tilt = 6; - this->has_tilt_ = value; - return true; - case 8: - // bool stop = 8; - this->stop_ = value; - default: - return false; - } -} -bool CoverCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: - // fixed32 key = 1; - this->key_ = value; - return true; - case 5: - // float position = 5; - this->position_ = as_float(value); - return true; - case 7: - // float tilt = 7; - this->tilt_ = as_float(value); - return true; - default: - return false; - } -} -APIMessageType CoverCommandRequest::message_type() const { return APIMessageType ::COVER_COMMAND_REQUEST; } -uint32_t CoverCommandRequest::get_key() const { return this->key_; } -optional<LegacyCoverCommand> CoverCommandRequest::get_legacy_command() const { - if (!this->has_legacy_command_) - return {}; - return this->legacy_command_; -} -optional<float> CoverCommandRequest::get_position() const { - if (!this->has_position_) - return {}; - return this->position_; -} -optional<float> CoverCommandRequest::get_tilt() const { - if (!this->has_tilt_) - return {}; - return this->tilt_; -} -#endif - -#ifdef USE_FAN -bool FanCommandRequest::decode_varint(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 2: - // bool has_state = 2; - this->has_state_ = value; - return true; - case 3: - // bool state = 3; - this->state_ = value; - return true; - case 4: - // bool has_speed = 4; - this->has_speed_ = value; - return true; - case 5: - // FanSpeed speed = 5; - this->speed_ = static_cast<fan::FanSpeed>(value); - return true; - case 6: - // bool has_oscillating = 6; - this->has_oscillating_ = value; - return true; - case 7: - // bool oscillating = 7; - this->oscillating_ = value; - return true; - default: - return false; - } -} -bool FanCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: - // fixed32 key = 1; - this->key_ = value; - return true; - default: - return false; - } -} -APIMessageType FanCommandRequest::message_type() const { return APIMessageType::FAN_COMMAND_REQUEST; } -uint32_t FanCommandRequest::get_key() const { return this->key_; } -optional<bool> FanCommandRequest::get_state() const { - if (!this->has_state_) - return {}; - return this->state_; -} -optional<fan::FanSpeed> FanCommandRequest::get_speed() const { - if (!this->has_speed_) - return {}; - return this->speed_; -} -optional<bool> FanCommandRequest::get_oscillating() const { - if (!this->has_oscillating_) - return {}; - return this->oscillating_; -} -#endif - -#ifdef USE_LIGHT -bool LightCommandRequest::decode_varint(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 2: - // bool has_state = 2; - this->has_state_ = value; - return true; - case 3: - // bool state = 3; - this->state_ = value; - return true; - case 4: - // bool has_brightness = 4; - this->has_brightness_ = value; - return true; - case 6: - // bool has_rgb = 6; - this->has_rgb_ = value; - return true; - case 10: - // bool has_white = 10; - this->has_white_ = value; - return true; - case 12: - // bool has_color_temperature = 12; - this->has_color_temperature_ = value; - return true; - case 14: - // bool has_transition_length = 14; - this->has_transition_length_ = value; - return true; - case 15: - // uint32 transition_length = 15; - this->transition_length_ = value; - return true; - case 16: - // bool has_flash_length = 16; - this->has_flash_length_ = value; - return true; - case 17: - // uint32 flash_length = 17; - this->flash_length_ = value; - return true; - case 18: - // bool has_effect = 18; - this->has_effect_ = value; - return true; - default: - return false; - } -} -bool LightCommandRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) { - switch (field_id) { - case 19: - // string effect = 19; - this->effect_ = as_string(value, len); - return true; - default: - return false; - } -} -bool LightCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: - // fixed32 key = 1; - this->key_ = value; - return true; - case 5: - // float brightness = 5; - this->brightness_ = as_float(value); - return true; - case 7: - // float red = 7; - this->red_ = as_float(value); - return true; - case 8: - // float green = 8; - this->green_ = as_float(value); - return true; - case 9: - // float blue = 9; - this->blue_ = as_float(value); - return true; - case 11: - // float white = 11; - this->white_ = as_float(value); - return true; - case 13: - // float color_temperature = 13; - this->color_temperature_ = as_float(value); - return true; - default: - return false; - } -} -APIMessageType LightCommandRequest::message_type() const { return APIMessageType::LIGHT_COMMAND_REQUEST; } -uint32_t LightCommandRequest::get_key() const { return this->key_; } -optional<bool> LightCommandRequest::get_state() const { - if (!this->has_state_) - return {}; - return this->state_; -} -optional<float> LightCommandRequest::get_brightness() const { - if (!this->has_brightness_) - return {}; - return this->brightness_; -} -optional<float> LightCommandRequest::get_red() const { - if (!this->has_rgb_) - return {}; - return this->red_; -} -optional<float> LightCommandRequest::get_green() const { - if (!this->has_rgb_) - return {}; - return this->green_; -} -optional<float> LightCommandRequest::get_blue() const { - if (!this->has_rgb_) - return {}; - return this->blue_; -} -optional<float> LightCommandRequest::get_white() const { - if (!this->has_white_) - return {}; - return this->white_; -} -optional<float> LightCommandRequest::get_color_temperature() const { - if (!this->has_color_temperature_) - return {}; - return this->color_temperature_; -} -optional<uint32_t> LightCommandRequest::get_transition_length() const { - if (!this->has_transition_length_) - return {}; - return this->transition_length_; -} -optional<uint32_t> LightCommandRequest::get_flash_length() const { - if (!this->has_flash_length_) - return {}; - return this->flash_length_; -} -optional<std::string> LightCommandRequest::get_effect() const { - if (!this->has_effect_) - return {}; - return this->effect_; -} -#endif - -#ifdef USE_SWITCH -bool SwitchCommandRequest::decode_varint(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 2: - // bool state = 2; - this->state_ = value; - return true; - default: - return false; - } -} -bool SwitchCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: - // fixed32 key = 1; - this->key_ = value; - return true; - default: - return false; - } -} -APIMessageType SwitchCommandRequest::message_type() const { return APIMessageType::SWITCH_COMMAND_REQUEST; } -uint32_t SwitchCommandRequest::get_key() const { return this->key_; } -bool SwitchCommandRequest::get_state() const { return this->state_; } -#endif - -#ifdef USE_ESP32_CAMERA -bool CameraImageRequest::get_single() const { return this->single_; } -bool CameraImageRequest::get_stream() const { return this->stream_; } -bool CameraImageRequest::decode_varint(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: - // bool single = 1; - this->single_ = value; - return true; - case 2: - // bool stream = 2; - this->stream_ = value; - return true; - default: - return false; - } -} -APIMessageType CameraImageRequest::message_type() const { return APIMessageType::CAMERA_IMAGE_REQUEST; } -#endif - -#ifdef USE_CLIMATE -bool ClimateCommandRequest::decode_varint(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 2: - // bool has_mode = 2; - this->has_mode_ = value; - return true; - case 3: - // ClimateMode mode = 3; - this->mode_ = static_cast<climate::ClimateMode>(value); - return true; - case 4: - // bool has_target_temperature = 4; - this->has_target_temperature_ = value; - return true; - case 6: - // bool has_target_temperature_low = 6; - this->has_target_temperature_low_ = value; - return true; - case 8: - // bool has_target_temperature_high = 8; - this->has_target_temperature_high_ = value; - return true; - case 10: - // bool has_away = 10; - this->has_away_ = value; - return true; - case 11: - // bool away = 11; - this->away_ = value; - return true; - default: - return false; - } -} -bool ClimateCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: - // fixed32 key = 1; - this->key_ = value; - return true; - case 5: - // float target_temperature = 5; - this->target_temperature_ = as_float(value); - return true; - case 7: - // float target_temperature_low = 7; - this->target_temperature_low_ = as_float(value); - return true; - case 9: - // float target_temperature_high = 9; - this->target_temperature_high_ = as_float(value); - return true; - default: - return false; - } -} -APIMessageType ClimateCommandRequest::message_type() const { return APIMessageType::CLIMATE_COMMAND_REQUEST; } -uint32_t ClimateCommandRequest::get_key() const { return this->key_; } -optional<climate::ClimateMode> ClimateCommandRequest::get_mode() const { - if (!this->has_mode_) - return {}; - return this->mode_; -} -optional<float> ClimateCommandRequest::get_target_temperature() const { - if (!this->has_target_temperature_) - return {}; - return this->target_temperature_; -} -optional<float> ClimateCommandRequest::get_target_temperature_low() const { - if (!this->has_target_temperature_low_) - return {}; - return this->target_temperature_low_; -} -optional<float> ClimateCommandRequest::get_target_temperature_high() const { - if (!this->has_target_temperature_high_) - return {}; - return this->target_temperature_high_; -} -optional<bool> ClimateCommandRequest::get_away() const { - if (!this->has_away_) - return {}; - return this->away_; -} -#endif - -} // namespace api -} // namespace esphome diff --git a/esphome/components/api/command_messages.h b/esphome/components/api/command_messages.h deleted file mode 100644 index 09ca9cb126..0000000000 --- a/esphome/components/api/command_messages.h +++ /dev/null @@ -1,162 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/defines.h" -#include "api_message.h" - -namespace esphome { -namespace api { - -#ifdef USE_COVER -enum LegacyCoverCommand { - LEGACY_COVER_COMMAND_OPEN = 0, - LEGACY_COVER_COMMAND_CLOSE = 1, - LEGACY_COVER_COMMAND_STOP = 2, -}; - -class CoverCommandRequest : public APIMessage { - public: - bool decode_varint(uint32_t field_id, uint32_t value) override; - bool decode_32bit(uint32_t field_id, uint32_t value) override; - APIMessageType message_type() const override; - uint32_t get_key() const; - optional<LegacyCoverCommand> get_legacy_command() const; - optional<float> get_position() const; - optional<float> get_tilt() const; - bool get_stop() const { return this->stop_; } - - protected: - uint32_t key_{0}; - bool has_legacy_command_{false}; - LegacyCoverCommand legacy_command_{LEGACY_COVER_COMMAND_OPEN}; - bool has_position_{false}; - float position_{0.0f}; - bool has_tilt_{false}; - float tilt_{0.0f}; - bool stop_{false}; -}; -#endif - -#ifdef USE_FAN -class FanCommandRequest : public APIMessage { - public: - bool decode_varint(uint32_t field_id, uint32_t value) override; - bool decode_32bit(uint32_t field_id, uint32_t value) override; - APIMessageType message_type() const override; - uint32_t get_key() const; - optional<bool> get_state() const; - optional<fan::FanSpeed> get_speed() const; - optional<bool> get_oscillating() const; - - protected: - uint32_t key_{0}; - bool has_state_{false}; - bool state_{false}; - bool has_speed_{false}; - fan::FanSpeed speed_{fan::FAN_SPEED_LOW}; - bool has_oscillating_{false}; - bool oscillating_{false}; -}; -#endif - -#ifdef USE_LIGHT -class LightCommandRequest : public APIMessage { - public: - bool decode_varint(uint32_t field_id, uint32_t value) override; - bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override; - bool decode_32bit(uint32_t field_id, uint32_t value) override; - APIMessageType message_type() const override; - uint32_t get_key() const; - optional<bool> get_state() const; - optional<float> get_brightness() const; - optional<float> get_red() const; - optional<float> get_green() const; - optional<float> get_blue() const; - optional<float> get_white() const; - optional<float> get_color_temperature() const; - optional<uint32_t> get_transition_length() const; - optional<uint32_t> get_flash_length() const; - optional<std::string> get_effect() const; - - protected: - uint32_t key_{0}; - bool has_state_{false}; - bool state_{false}; - bool has_brightness_{false}; - float brightness_{0.0f}; - bool has_rgb_{false}; - float red_{0.0f}; - float green_{0.0f}; - float blue_{0.0f}; - bool has_white_{false}; - float white_{0.0f}; - bool has_color_temperature_{false}; - float color_temperature_{0.0f}; - bool has_transition_length_{false}; - uint32_t transition_length_{0}; - bool has_flash_length_{false}; - uint32_t flash_length_{0}; - bool has_effect_{false}; - std::string effect_{}; -}; -#endif - -#ifdef USE_SWITCH -class SwitchCommandRequest : public APIMessage { - public: - bool decode_varint(uint32_t field_id, uint32_t value) override; - bool decode_32bit(uint32_t field_id, uint32_t value) override; - APIMessageType message_type() const override; - uint32_t get_key() const; - bool get_state() const; - - protected: - uint32_t key_{0}; - bool state_{false}; -}; -#endif - -#ifdef USE_ESP32_CAMERA -class CameraImageRequest : public APIMessage { - public: - bool decode_varint(uint32_t field_id, uint32_t value) override; - bool get_single() const; - bool get_stream() const; - APIMessageType message_type() const override; - - protected: - bool single_{false}; - bool stream_{false}; -}; -#endif - -#ifdef USE_CLIMATE -class ClimateCommandRequest : public APIMessage { - public: - bool decode_varint(uint32_t field_id, uint32_t value) override; - bool decode_32bit(uint32_t field_id, uint32_t value) override; - APIMessageType message_type() const override; - uint32_t get_key() const; - optional<climate::ClimateMode> get_mode() const; - optional<float> get_target_temperature() const; - optional<float> get_target_temperature_low() const; - optional<float> get_target_temperature_high() const; - optional<bool> get_away() const; - - protected: - uint32_t key_{0}; - bool has_mode_{false}; - climate::ClimateMode mode_{climate::CLIMATE_MODE_OFF}; - bool has_target_temperature_{false}; - float target_temperature_{0.0f}; - bool has_target_temperature_low_{false}; - float target_temperature_low_{0.0f}; - bool has_target_temperature_high_{false}; - float target_temperature_high_{0.0f}; - bool has_away_{false}; - bool away_{false}; -}; -#endif - -} // namespace api -} // namespace esphome diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h new file mode 100644 index 0000000000..aac91244b6 --- /dev/null +++ b/esphome/components/api/custom_api_device.h @@ -0,0 +1,214 @@ +#pragma once + +#include <map> +#include "user_services.h" +#include "api_server.h" + +namespace esphome { +namespace api { + +template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> { + public: + CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj, + void (T::*callback)(Ts...)) + : UserServiceBase<Ts...>(name, arg_names), obj_(obj), callback_(callback) {} + + protected: + void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT + + T *obj_; + void (T::*callback_)(Ts...); +}; + +class CustomAPIDevice { + public: + /// Return if a client (such as Home Assistant) is connected to the native API. + bool is_connected() const { return global_api_server->is_connected(); } + + /** Register a custom native API service that will show up in Home Assistant. + * + * Usage: + * + * ```cpp + * void setup() override { + * register_service(&CustomNativeAPI::on_start_washer_cycle, "start_washer_cycle", + * {"cycle_length"}); + * } + * + * void on_start_washer_cycle(int cycle_length) { + * // Start washer cycle. + * } + * ``` + * + * @tparam T The class type creating the service, automatically deduced from the function pointer. + * @tparam Ts The argument types for the service, automatically deduced from the function arguments. + * @param callback The member function to call when the service is triggered. + * @param name The name of the service to register. + * @param arg_names The name of the arguments for the service, must match the arguments of the function. + */ + template<typename T, typename... Ts> + void register_service(void (T::*callback)(Ts...), const std::string &name, + const std::array<std::string, sizeof...(Ts)> &arg_names) { + auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); + global_api_server->register_user_service(service); + } + + /** Register a custom native API service that will show up in Home Assistant. + * + * Usage: + * + * ```cpp + * void setup() override { + * register_service(&CustomNativeAPI::on_hello_world, "hello_world"); + * } + * + * void on_hello_world() { + * // Hello World service called. + * } + * ``` + * + * @tparam T The class type creating the service, automatically deduced from the function pointer. + * @param callback The member function to call when the service is triggered. + * @param name The name of the arguments for the service, must match the arguments of the function. + */ + template<typename T> void register_service(void (T::*callback)(), const std::string &name) { + auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); + global_api_server->register_user_service(service); + } + + /** Subscribe to the state of an entity from Home Assistant. + * + * Usage: + * + * ```cpp + * void setup() override { + * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast"); + * } + * + * void on_state_changed(std::string state) { + * // State of sensor.weather_forecast is `state` + * } + * ``` + * + * @tparam T The class type creating the service, automatically deduced from the function pointer. + * @param callback The member function to call when the entity state changes. + * @param entity_id The entity_id to track. + */ + template<typename T> + void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id) { + auto f = std::bind(callback, (T *) this, std::placeholders::_1); + global_api_server->subscribe_home_assistant_state(entity_id, f); + } + + /** Subscribe to the state of an entity from Home Assistant. + * + * Usage: + * + * ```cpp + * void setup() override { + * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast"); + * } + * + * void on_state_changed(std::string entity_id, std::string state) { + * // State of `entity_id` is `state` + * } + * ``` + * + * @tparam T The class type creating the service, automatically deduced from the function pointer. + * @param callback The member function to call when the entity state changes. + * @param entity_id The entity_id to track. + */ + template<typename T> + void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id) { + auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); + global_api_server->subscribe_home_assistant_state(entity_id, f); + } + + /** Call a Home Assistant service from ESPHome. + * + * Usage: + * + * ```cpp + * call_homeassistant_service("homeassistant.restart"); + * ``` + * + * @param service_name The service to call. + */ + void call_homeassistant_service(const std::string &service_name) { + HomeassistantServiceResponse resp; + resp.service = service_name; + global_api_server->send_homeassistant_service_call(resp); + } + + /** Call a Home Assistant service from ESPHome. + * + * Usage: + * + * ```cpp + * call_homeassistant_service("light.turn_on", { + * {"entity_id", "light.my_light"}, + * {"brightness", "127"}, + * }); + * ``` + * + * @param service_name The service to call. + * @param data The data for the service call, mapping from string to string. + */ + void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) { + HomeassistantServiceResponse resp; + resp.service = service_name; + for (auto &it : data) { + HomeassistantServiceMap kv; + kv.key = it.first; + kv.value = it.second; + resp.data.push_back(kv); + } + global_api_server->send_homeassistant_service_call(resp); + } + + /** Fire an ESPHome event in Home Assistant. + * + * Usage: + * + * ```cpp + * fire_homeassistant_event("esphome.something_happened"); + * ``` + * + * @param event_name The event to fire. + */ + void fire_homeassistant_event(const std::string &event_name) { + HomeassistantServiceResponse resp; + resp.service = event_name; + resp.is_event = true; + global_api_server->send_homeassistant_service_call(resp); + } + + /** Fire an ESPHome event in Home Assistant. + * + * Usage: + * + * ```cpp + * fire_homeassistant_event("esphome.something_happened", { + * {"my_value", "500"}, + * }); + * ``` + * + * @param event_name The event to fire. + * @param data The data for the event, mapping from string to string. + */ + void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) { + HomeassistantServiceResponse resp; + resp.service = service_name; + resp.is_event = true; + for (auto &it : data) { + HomeassistantServiceMap kv; + kv.key = it.first; + kv.value = it.second; + resp.data.push_back(kv); + } + global_api_server->send_homeassistant_service_call(resp); + } +}; + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h new file mode 100644 index 0000000000..d68dac3b61 --- /dev/null +++ b/esphome/components/api/homeassistant_service.h @@ -0,0 +1,66 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/automation.h" +#include "api_pb2.h" +#include "api_server.h" + +namespace esphome { +namespace api { + +template<typename... Ts> class TemplatableKeyValuePair { + public: + template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} + std::string key; + TemplatableStringValue<Ts...> value; +}; + +template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> { + public: + explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {} + + TEMPLATABLE_STRING_VALUE(service); + template<typename T> void add_data(std::string key, T value) { + this->data_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); + } + template<typename T> void add_data_template(std::string key, T value) { + this->data_template_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); + } + template<typename T> void add_variable(std::string key, T value) { + this->variables_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); + } + void play(Ts... x) override { + HomeassistantServiceResponse resp; + resp.service = this->service_.value(x...); + resp.is_event = this->is_event_; + for (auto &it : this->data_) { + HomeassistantServiceMap kv; + kv.key = it.key; + kv.value = it.value.value(x...); + resp.data.push_back(kv); + } + for (auto &it : this->data_template_) { + HomeassistantServiceMap kv; + kv.key = it.key; + kv.value = it.value.value(x...); + resp.data_template.push_back(kv); + } + for (auto &it : this->variables_) { + HomeassistantServiceMap kv; + kv.key = it.key; + kv.value = it.value.value(x...); + resp.variables.push_back(kv); + } + this->parent_->send_homeassistant_service_call(resp); + } + + protected: + APIServer *parent_; + bool is_event_; + std::vector<TemplatableKeyValuePair<Ts...>> data_; + std::vector<TemplatableKeyValuePair<Ts...>> data_template_; + std::vector<TemplatableKeyValuePair<Ts...>> variables_; +}; + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index ae63d70448..d4245136ae 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -2,189 +2,54 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "api_connection.h" namespace esphome { namespace api { -std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) { - return App.get_name() + component_type + nameable->get_object_id(); -} - #ifdef USE_BINARY_SENSOR bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - auto buffer = this->client_->get_buffer(); - buffer.encode_nameable(binary_sensor); - // string unique_id = 4; - buffer.encode_string(4, get_default_unique_id("binary_sensor", binary_sensor)); - // string device_class = 5; - buffer.encode_string(5, binary_sensor->get_device_class()); - // bool is_status_binary_sensor = 6; - buffer.encode_bool(6, binary_sensor->is_status_binary_sensor()); - return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_BINARY_SENSOR_RESPONSE); + return this->client_->send_binary_sensor_info(binary_sensor); } #endif #ifdef USE_COVER -bool ListEntitiesIterator::on_cover(cover::Cover *cover) { - auto buffer = this->client_->get_buffer(); - buffer.encode_nameable(cover); - // string unique_id = 4; - buffer.encode_string(4, get_default_unique_id("cover", cover)); - auto traits = cover->get_traits(); - - // bool assumed_state = 5; - buffer.encode_bool(5, traits.get_is_assumed_state()); - // bool supports_position = 6; - buffer.encode_bool(6, traits.get_supports_position()); - // bool supports_tilt = 7; - buffer.encode_bool(7, traits.get_supports_tilt()); - // string device_class = 8; - buffer.encode_string(8, cover->get_device_class()); - return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_COVER_RESPONSE); -} +bool ListEntitiesIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_info(cover); } #endif #ifdef USE_FAN -bool ListEntitiesIterator::on_fan(fan::FanState *fan) { - auto buffer = this->client_->get_buffer(); - buffer.encode_nameable(fan); - // string unique_id = 4; - buffer.encode_string(4, get_default_unique_id("fan", fan)); - // bool supports_oscillation = 5; - buffer.encode_bool(5, fan->get_traits().supports_oscillation()); - // bool supports_speed = 6; - buffer.encode_bool(6, fan->get_traits().supports_speed()); - return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_FAN_RESPONSE); -} +bool ListEntitiesIterator::on_fan(fan::FanState *fan) { return this->client_->send_fan_info(fan); } #endif #ifdef USE_LIGHT -bool ListEntitiesIterator::on_light(light::LightState *light) { - auto buffer = this->client_->get_buffer(); - buffer.encode_nameable(light); - // string unique_id = 4; - buffer.encode_string(4, get_default_unique_id("light", light)); - // bool supports_brightness = 5; - auto traits = light->get_traits(); - buffer.encode_bool(5, traits.get_supports_brightness()); - // bool supports_rgb = 6; - buffer.encode_bool(6, traits.get_supports_rgb()); - // bool supports_white_value = 7; - buffer.encode_bool(7, traits.get_supports_rgb_white_value()); - // bool supports_color_temperature = 8; - buffer.encode_bool(8, traits.get_supports_color_temperature()); - if (traits.get_supports_color_temperature()) { - // float min_mireds = 9; - buffer.encode_float(9, traits.get_min_mireds()); - // float max_mireds = 10; - buffer.encode_float(10, traits.get_max_mireds()); - } - // repeated string effects = 11; - if (light->supports_effects()) { - buffer.encode_string(11, "None"); - for (auto *effect : light->get_effects()) { - buffer.encode_string(11, effect->get_name()); - } - } - return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_LIGHT_RESPONSE); -} +bool ListEntitiesIterator::on_light(light::LightState *light) { return this->client_->send_light_info(light); } #endif #ifdef USE_SENSOR -bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { - auto buffer = this->client_->get_buffer(); - buffer.encode_nameable(sensor); - // string unique_id = 4; - std::string unique_id = sensor->unique_id(); - if (unique_id.empty()) - unique_id = get_default_unique_id("sensor", sensor); - buffer.encode_string(4, unique_id); - // string icon = 5; - buffer.encode_string(5, sensor->get_icon()); - // string unit_of_measurement = 6; - buffer.encode_string(6, sensor->get_unit_of_measurement()); - // int32 accuracy_decimals = 7; - buffer.encode_int32(7, sensor->get_accuracy_decimals()); - return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_SENSOR_RESPONSE); -} +bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_info(sensor); } #endif #ifdef USE_SWITCH -bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { - auto buffer = this->client_->get_buffer(); - buffer.encode_nameable(a_switch); - // string unique_id = 4; - buffer.encode_string(4, get_default_unique_id("switch", a_switch)); - // string icon = 5; - buffer.encode_string(5, a_switch->get_icon()); - // bool assumed_state = 6; - buffer.encode_bool(6, a_switch->assumed_state()); - return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_SWITCH_RESPONSE); -} +bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_info(a_switch); } #endif #ifdef USE_TEXT_SENSOR bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - auto buffer = this->client_->get_buffer(); - buffer.encode_nameable(text_sensor); - // string unique_id = 4; - std::string unique_id = text_sensor->unique_id(); - if (unique_id.empty()) - unique_id = get_default_unique_id("text_sensor", text_sensor); - buffer.encode_string(4, unique_id); - // string icon = 5; - buffer.encode_string(5, text_sensor->get_icon()); - return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_TEXT_SENSOR_RESPONSE); + return this->client_->send_text_sensor_info(text_sensor); } #endif -bool ListEntitiesIterator::on_end() { - return this->client_->send_empty_message(APIMessageType::LIST_ENTITIES_DONE_RESPONSE); -} +bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client) : ComponentIterator(server), client_(client) {} bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { - auto buffer = this->client_->get_buffer(); - service->encode_list_service_response(buffer); - return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_SERVICE_RESPONSE); + auto resp = service->encode_list_service_response(); + return this->client_->send_list_entities_services_response(resp); } #ifdef USE_ESP32_CAMERA bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) { - auto buffer = this->client_->get_buffer(); - buffer.encode_nameable(camera); - // string unique_id = 4; - buffer.encode_string(4, get_default_unique_id("camera", camera)); - return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_CAMERA_RESPONSE); + return this->client_->send_camera_info(camera); } #endif #ifdef USE_CLIMATE -bool ListEntitiesIterator::on_climate(climate::Climate *climate) { - auto buffer = this->client_->get_buffer(); - buffer.encode_nameable(climate); - // string unique_id = 4; - buffer.encode_string(4, get_default_unique_id("climate", climate)); - - auto traits = climate->get_traits(); - // bool supports_current_temperature = 5; - buffer.encode_bool(5, traits.get_supports_current_temperature()); - // bool supports_two_point_target_temperature = 6; - buffer.encode_bool(6, traits.get_supports_two_point_target_temperature()); - // repeated ClimateMode supported_modes = 7; - for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, - climate::CLIMATE_MODE_HEAT}) { - if (traits.supports_mode(mode)) - buffer.encode_uint32(7, mode, true); - } - - // float visual_min_temperature = 8; - buffer.encode_float(8, traits.get_visual_min_temperature()); - // float visual_max_temperature = 9; - buffer.encode_float(9, traits.get_visual_max_temperature()); - // float visual_temperature_step = 10; - buffer.encode_float(10, traits.get_visual_temperature_step()); - // bool supports_away = 11; - buffer.encode_bool(11, traits.get_supports_away()); - return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_CLIMATE_RESPONSE); -} +bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_info(climate); } #endif -APIMessageType ListEntitiesRequest::message_type() const { return APIMessageType::LIST_ENTITIES_REQUEST; } - } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index c1c93e91f8..6b10a72fdf 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -2,16 +2,11 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "api_message.h" +#include "util.h" namespace esphome { namespace api { -class ListEntitiesRequest : public APIMessage { - public: - APIMessageType message_type() const override; -}; - class APIConnection; class ListEntitiesIterator : public ComponentIterator { diff --git a/esphome/components/api/api_message.cpp b/esphome/components/api/proto.cpp similarity index 64% rename from esphome/components/api/api_message.cpp rename to esphome/components/api/proto.cpp index 157ddba2b5..3d2f669f54 100644 --- a/esphome/components/api/api_message.cpp +++ b/esphome/components/api/proto.cpp @@ -1,61 +1,59 @@ -#include "api_message.h" +#include "proto.h" +#include "util.h" #include "esphome/core/log.h" namespace esphome { namespace api { -static const char *TAG = "api.message"; +static const char *TAG = "api.proto"; -bool APIMessage::decode_varint(uint32_t field_id, uint32_t value) { return false; } -bool APIMessage::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) { return false; } -bool APIMessage::decode_32bit(uint32_t field_id, uint32_t value) { return false; } -void APIMessage::encode(APIBuffer &buffer) {} -void APIMessage::decode(const uint8_t *buffer, size_t length) { +void ProtoMessage::decode(const uint8_t *buffer, size_t length) { uint32_t i = 0; bool error = false; while (i < length) { uint32_t consumed; - auto res = proto_decode_varuint32(&buffer[i], length - i, &consumed); + auto res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid field start at %u", i); break; } - uint32_t field_type = (*res) & 0b111; - uint32_t field_id = (*res) >> 3; + uint32_t field_type = (res->as_uint32()) & 0b111; + uint32_t field_id = (res->as_uint32()) >> 3; i += consumed; switch (field_type) { case 0: { // VarInt - res = proto_decode_varuint32(&buffer[i], length - i, &consumed); + res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid VarInt at %u", i); error = true; break; } if (!this->decode_varint(field_id, *res)) { - ESP_LOGV(TAG, "Cannot decode VarInt field %u with value %u!", field_id, *res); + ESP_LOGV(TAG, "Cannot decode VarInt field %u with value %u!", field_id, res->as_uint32()); } i += consumed; break; } case 2: { // Length-delimited - res = proto_decode_varuint32(&buffer[i], length - i, &consumed); + res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid Length Delimited at %u", i); error = true; break; } + uint32_t field_length = res->as_uint32(); i += consumed; - if (*res > length - i) { + if (field_length > length - i) { ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %u", i); error = true; break; } - if (!this->decode_length_delimited(field_id, &buffer[i], *res)) { + if (!this->decode_length(field_id, ProtoLengthDelimited(&buffer[i], field_length))) { ESP_LOGV(TAG, "Cannot decode Length Delimited field %u!", field_id); } - i += *res; + i += field_length; break; } case 5: { // 32-bit @@ -66,7 +64,7 @@ void APIMessage::decode(const uint8_t *buffer, size_t length) { } uint32_t val = (uint32_t(buffer[i]) << 0) | (uint32_t(buffer[i + 1]) << 8) | (uint32_t(buffer[i + 2]) << 16) | (uint32_t(buffer[i + 3]) << 24); - if (!this->decode_32bit(field_id, val)) { + if (!this->decode_32bit(field_id, Proto32Bit(val))) { ESP_LOGV(TAG, "Cannot decode 32-bit field %u with value %u!", field_id, val); } i += 4; @@ -83,5 +81,11 @@ void APIMessage::decode(const uint8_t *buffer, size_t length) { } } +std::string ProtoMessage::dump() const { + std::string out; + this->dump_to(out); + return out; +} + } // namespace api } // namespace esphome diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h new file mode 100644 index 0000000000..b0160bf240 --- /dev/null +++ b/esphome/components/api/proto.h @@ -0,0 +1,279 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace api { + +/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit +class ProtoVarInt { + public: + ProtoVarInt() : value_(0) {} + explicit ProtoVarInt(uint64_t value) : value_(value) {} + + static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { + if (consumed != nullptr) + *consumed = 0; + + if (len == 0) + return {}; + + uint64_t result = 0; + uint8_t bitpos = 0; + + for (uint32_t i = 0; i < len; i++) { + uint8_t val = buffer[i]; + result |= uint64_t(val & 0x7F) << uint64_t(bitpos); + bitpos += 7; + if ((val & 0x80) == 0) { + if (consumed != nullptr) + *consumed = i + 1; + return ProtoVarInt(result); + } + } + + return {}; + } + + uint32_t as_uint32() const { return this->value_; } + uint64_t as_uint64() const { return this->value_; } + bool as_bool() const { return this->value_; } + template<typename T> T as_enum() const { return static_cast<T>(this->as_uint32()); } + int32_t as_int32() const { + // Not ZigZag encoded + return static_cast<int32_t>(this->as_int64()); + } + int64_t as_int64() const { + // Not ZigZag encoded + return static_cast<int64_t>(this->value_); + } + int32_t as_sint32() const { + // with ZigZag encoding + if (this->value_ & 1) + return static_cast<int32_t>(~(this->value_ >> 1)); + else + return static_cast<int32_t>(this->value_ >> 1); + } + int64_t as_sint64() const { + // with ZigZag encoding + if (this->value_ & 1) + return static_cast<int64_t>(~(this->value_ >> 1)); + else + return static_cast<int64_t>(this->value_ >> 1); + } + void encode(std::vector<uint8_t> &out) { + uint32_t val = this->value_; + if (val <= 0x7F) { + out.push_back(val); + return; + } + while (val) { + uint8_t temp = val & 0x7F; + val >>= 7; + if (val) { + out.push_back(temp | 0x80); + } else { + out.push_back(temp); + } + } + } + + protected: + uint64_t value_; +}; + +class ProtoLengthDelimited { + public: + explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {} + std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); } + template<class C> C as_message() const { + auto msg = C(); + msg.decode(this->value_, this->length_); + return msg; + } + + protected: + const uint8_t *const value_; + const size_t length_; +}; + +class Proto32Bit { + public: + explicit Proto32Bit(uint32_t value) : value_(value) {} + uint32_t as_fixed32() const { return this->value_; } + int32_t as_sfixed32() const { return static_cast<int32_t>(this->value_); } + float as_float() const { + union { + uint32_t raw; + float value; + } s{}; + s.raw = this->value_; + return s.value; + } + + protected: + const uint32_t value_; +}; + +class Proto64Bit { + public: + explicit Proto64Bit(uint64_t value) : value_(value) {} + uint64_t as_fixed64() const { return this->value_; } + int64_t as_sfixed64() const { return static_cast<int64_t>(this->value_); } + double as_double() const { + union { + uint64_t raw; + double value; + } s{}; + s.raw = this->value_; + return s.value; + } + + protected: + const uint64_t value_; +}; + +class ProtoWriteBuffer { + public: + ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {} + void write(uint8_t value) { this->buffer_->push_back(value); } + void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); } + void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } + void encode_field_raw(uint32_t field_id, uint32_t type) { + uint32_t val = (field_id << 3) | (type & 0b111); + this->encode_varint_raw(val); + } + void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { + if (len == 0 && !force) + return; + + this->encode_field_raw(field_id, 2); + this->encode_varint_raw(len); + auto *data = reinterpret_cast<const uint8_t *>(string); + for (size_t i = 0; i < len; i++) + this->write(data[i]); + } + void encode_string(uint32_t field_id, const std::string &value, bool force = false) { + this->encode_string(field_id, value.data(), value.size()); + } + void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { + this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force); + } + void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) { + if (value == 0 && !force) + return; + this->encode_field_raw(field_id, 0); + this->encode_varint_raw(value); + } + void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) { + if (value == 0 && !force) + return; + this->encode_field_raw(field_id, 0); + this->encode_varint_raw(ProtoVarInt(value)); + } + void encode_bool(uint32_t field_id, bool value, bool force = false) { + if (!value && !force) + return; + this->encode_field_raw(field_id, 0); + this->write(0x01); + } + void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { + if (value == 0 && !force) + return; + + this->encode_field_raw(field_id, 5); + this->write((value >> 0) & 0xFF); + this->write((value >> 8) & 0xFF); + this->write((value >> 16) & 0xFF); + this->write((value >> 24) & 0xFF); + } + template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) { + this->encode_uint32(field_id, static_cast<uint32_t>(value), force); + } + void encode_float(uint32_t field_id, float value, bool force = false) { + if (value == 0.0f && !force) + return; + + union { + float value; + uint32_t raw; + } val{}; + val.value = value; + this->encode_fixed32(field_id, val.raw); + } + void encode_int32(uint32_t field_id, int32_t value, bool force = false) { + if (value < 0) { + // negative int32 is always 10 byte long + this->encode_int64(field_id, value, force); + return; + } + this->encode_uint32(field_id, static_cast<uint32_t>(value), force); + } + void encode_int64(uint32_t field_id, int64_t value, bool force = false) { + this->encode_uint64(field_id, static_cast<uint64_t>(value), force); + } + void encode_sint32(uint32_t field_id, int32_t value, bool force = false) { + uint32_t uvalue; + if (value < 0) + uvalue = ~(value << 1); + else + uvalue = value << 1; + this->encode_uint32(field_id, uvalue, force); + } + template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) { + this->encode_field_raw(field_id, 2); + size_t begin = this->buffer_->size(); + + value.encode(*this); + + const uint32_t nested_length = this->buffer_->size() - begin; + // add size varint + std::vector<uint8_t> var; + ProtoVarInt(nested_length).encode(var); + this->buffer_->insert(this->buffer_->begin() + begin, var.begin(), var.end()); + } + std::vector<uint8_t> *get_buffer() const { return buffer_; } + + protected: + std::vector<uint8_t> *buffer_; +}; + +class ProtoMessage { + public: + virtual void encode(ProtoWriteBuffer buffer) const = 0; + void decode(const uint8_t *buffer, size_t length); + std::string dump() const; + virtual void dump_to(std::string &out) const = 0; + + protected: + virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } + virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } + virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } + virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; } +}; + +template<typename T> const char *proto_enum_to_string(T value); + +class ProtoService { + public: + protected: + virtual bool is_authenticated() = 0; + virtual bool is_connection_setup() = 0; + virtual void on_fatal_error() = 0; + virtual void on_unauthenticated_access() = 0; + virtual void on_no_setup_connection() = 0; + virtual ProtoWriteBuffer create_buffer() = 0; + virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0; + virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; + virtual void set_nodelay(bool nodelay) = 0; + + template<class C> bool send_message_(const C &msg, uint32_t message_type) { + auto buffer = this->create_buffer(); + msg.encode(buffer); + return this->send_buffer(buffer, message_type); + } +}; + +} // namespace api +} // namespace esphome diff --git a/esphome/components/api/service_call_message.cpp b/esphome/components/api/service_call_message.cpp deleted file mode 100644 index f4c5741c94..0000000000 --- a/esphome/components/api/service_call_message.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "service_call_message.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace api { - -APIMessageType SubscribeServiceCallsRequest::message_type() const { - return APIMessageType::SUBSCRIBE_SERVICE_CALLS_REQUEST; -} - -APIMessageType ServiceCallResponse::message_type() const { return APIMessageType::SERVICE_CALL_RESPONSE; } -void ServiceCallResponse::encode(APIBuffer &buffer) { - // string service = 1; - buffer.encode_string(1, this->service_); - // map<string, string> data = 2; - for (auto &it : this->data_) { - auto nested = buffer.begin_nested(2); - buffer.encode_string(1, it.key); - buffer.encode_string(2, it.value); - buffer.end_nested(nested); - } - // map<string, string> data_template = 3; - for (auto &it : this->data_template_) { - auto nested = buffer.begin_nested(3); - buffer.encode_string(1, it.key); - buffer.encode_string(2, it.value); - buffer.end_nested(nested); - } - // map<string, string> variables = 4; - for (auto &it : this->variables_) { - auto nested = buffer.begin_nested(4); - buffer.encode_string(1, it.key); - buffer.encode_string(2, it.value()); - buffer.end_nested(nested); - } -} -void ServiceCallResponse::set_service(const std::string &service) { this->service_ = service; } -void ServiceCallResponse::set_data(const std::vector<KeyValuePair> &data) { this->data_ = data; } -void ServiceCallResponse::set_data_template(const std::vector<KeyValuePair> &data_template) { - this->data_template_ = data_template; -} -void ServiceCallResponse::set_variables(const std::vector<TemplatableKeyValuePair> &variables) { - this->variables_ = variables; -} - -KeyValuePair::KeyValuePair(const std::string &key, const std::string &value) : key(key), value(value) {} - -} // namespace api -} // namespace esphome diff --git a/esphome/components/api/service_call_message.h b/esphome/components/api/service_call_message.h deleted file mode 100644 index bb9d17a110..0000000000 --- a/esphome/components/api/service_call_message.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include "esphome/core/helpers.h" -#include "esphome/core/automation.h" -#include "api_message.h" - -namespace esphome { -namespace api { - -class SubscribeServiceCallsRequest : public APIMessage { - public: - APIMessageType message_type() const override; -}; - -class KeyValuePair { - public: - KeyValuePair(const std::string &key, const std::string &value); - - std::string key; - std::string value; -}; - -class TemplatableKeyValuePair { - public: - template<typename T> TemplatableKeyValuePair(std::string key, T func); - - std::string key; - std::function<std::string()> value; -}; -template<typename T> TemplatableKeyValuePair::TemplatableKeyValuePair(std::string key, T func) : key(key) { - this->value = [func]() -> std::string { return to_string(func()); }; -} - -class ServiceCallResponse : public APIMessage { - public: - APIMessageType message_type() const override; - - void encode(APIBuffer &buffer) override; - - void set_service(const std::string &service); - void set_data(const std::vector<KeyValuePair> &data); - void set_data_template(const std::vector<KeyValuePair> &data_template); - void set_variables(const std::vector<TemplatableKeyValuePair> &variables); - - protected: - std::string service_; - std::vector<KeyValuePair> data_; - std::vector<KeyValuePair> data_template_; - std::vector<TemplatableKeyValuePair> variables_; -}; - -} // namespace api -} // namespace esphome diff --git a/esphome/components/api/subscribe_logs.cpp b/esphome/components/api/subscribe_logs.cpp deleted file mode 100644 index f7f5be9fad..0000000000 --- a/esphome/components/api/subscribe_logs.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "subscribe_logs.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace api { - -APIMessageType SubscribeLogsRequest::message_type() const { return APIMessageType::SUBSCRIBE_LOGS_REQUEST; } -bool SubscribeLogsRequest::decode_varint(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: // LogLevel level = 1; - this->level_ = value; - return true; - case 2: // bool dump_config = 2; - this->dump_config_ = value; - return true; - default: - return false; - } -} -uint32_t SubscribeLogsRequest::get_level() const { return this->level_; } -void SubscribeLogsRequest::set_level(uint32_t level) { this->level_ = level; } -bool SubscribeLogsRequest::get_dump_config() const { return this->dump_config_; } -void SubscribeLogsRequest::set_dump_config(bool dump_config) { this->dump_config_ = dump_config; } - -} // namespace api -} // namespace esphome diff --git a/esphome/components/api/subscribe_logs.h b/esphome/components/api/subscribe_logs.h deleted file mode 100644 index f9b0db75b1..0000000000 --- a/esphome/components/api/subscribe_logs.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "api_message.h" - -namespace esphome { -namespace api { - -class SubscribeLogsRequest : public APIMessage { - public: - bool decode_varint(uint32_t field_id, uint32_t value) override; - APIMessageType message_type() const override; - uint32_t get_level() const; - void set_level(uint32_t level); - bool get_dump_config() const; - void set_dump_config(bool dump_config); - - protected: - uint32_t level_{6}; - bool dump_config_{false}; -}; - -} // namespace api -} // namespace esphome diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 8f312d5421..50d674bee2 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -1,4 +1,5 @@ #include "subscribe_state.h" +#include "api_connection.h" #include "esphome/core/log.h" namespace esphome { @@ -48,30 +49,5 @@ bool InitialStateIterator::on_climate(climate::Climate *climate) { return this-> InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client) : ComponentIterator(server), client_(client) {} -APIMessageType SubscribeStatesRequest::message_type() const { return APIMessageType::SUBSCRIBE_STATES_REQUEST; } - -bool HomeAssistantStateResponse::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) { - switch (field_id) { - case 1: - // string entity_id = 1; - this->entity_id_ = as_string(value, len); - return true; - case 2: - // string state = 2; - this->state_ = as_string(value, len); - return true; - default: - return false; - } -} -APIMessageType HomeAssistantStateResponse::message_type() const { - return APIMessageType::HOME_ASSISTANT_STATE_RESPONSE; -} -const std::string &HomeAssistantStateResponse::get_entity_id() const { return this->entity_id_; } -const std::string &HomeAssistantStateResponse::get_state() const { return this->state_; } -APIMessageType SubscribeHomeAssistantStatesRequest::message_type() const { - return APIMessageType::SUBSCRIBE_HOME_ASSISTANT_STATES_REQUEST; -} - } // namespace api } // namespace esphome diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 43b919f2e8..51b9c695e4 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -4,16 +4,10 @@ #include "esphome/core/controller.h" #include "esphome/core/defines.h" #include "util.h" -#include "api_message.h" namespace esphome { namespace api { -class SubscribeStatesRequest : public APIMessage { - public: - APIMessageType message_type() const override; -}; - class APIConnection; class InitialStateIterator : public ComponentIterator { @@ -47,23 +41,6 @@ class InitialStateIterator : public ComponentIterator { APIConnection *client_; }; -class SubscribeHomeAssistantStatesRequest : public APIMessage { - public: - APIMessageType message_type() const override; -}; - -class HomeAssistantStateResponse : public APIMessage { - public: - bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override; - APIMessageType message_type() const override; - const std::string &get_entity_id() const; - const std::string &get_state() const; - - protected: - std::string entity_id_; - std::string state_; -}; - } // namespace api } // namespace esphome diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index e1dd8932f8..0667d26ff6 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -4,71 +4,35 @@ namespace esphome { namespace api { -template<> bool ExecuteServiceArgument::get_value<bool>() { return this->value_bool_; } -template<> int ExecuteServiceArgument::get_value<int>() { return this->value_int_; } -template<> float ExecuteServiceArgument::get_value<float>() { return this->value_float_; } -template<> std::string ExecuteServiceArgument::get_value<std::string>() { return this->value_string_; } - -APIMessageType ExecuteServiceArgument::message_type() const { return APIMessageType::EXECUTE_SERVICE_REQUEST; } -bool ExecuteServiceArgument::decode_varint(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: // bool bool_ = 1; - this->value_bool_ = value; - return true; - case 2: // int32 int_ = 2; - this->value_int_ = value; - return true; - default: - return false; - } +template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; } +template<> int get_execute_arg_value<int>(const ExecuteServiceArgument &arg) { + if (arg.legacy_int != 0) + return arg.legacy_int; + return arg.int_; } -bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 3: // float float_ = 3; - this->value_float_ = as_float(value); - return true; - default: - return false; - } +template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; } +template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; } +template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) { + return arg.bool_array; } -bool ExecuteServiceArgument::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) { - switch (field_id) { - case 4: // string string_ = 4; - this->value_string_ = as_string(value, len); - return true; - default: - return false; - } +template<> std::vector<int> get_execute_arg_value<std::vector<int>>(const ExecuteServiceArgument &arg) { + return arg.int_array; +} +template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) { + return arg.float_array; +} +template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) { + return arg.string_array; } -bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: // fixed32 key = 1; - this->key_ = value; - return true; - default: - return false; - } -} -bool ExecuteServiceRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) { - switch (field_id) { - case 2: { // repeated ExecuteServiceArgument args = 2; - ExecuteServiceArgument arg; - arg.decode(value, len); - this->args_.push_back(arg); - return true; - } - default: - return false; - } -} -APIMessageType ExecuteServiceRequest::message_type() const { return APIMessageType::EXECUTE_SERVICE_REQUEST; } -const std::vector<ExecuteServiceArgument> &ExecuteServiceRequest::get_args() const { return this->args_; } -uint32_t ExecuteServiceRequest::get_key() const { return this->key_; } - -ServiceTypeArgument::ServiceTypeArgument(const std::string &name, ServiceArgType type) : name_(name), type_(type) {} -const std::string &ServiceTypeArgument::get_name() const { return this->name_; } -ServiceArgType ServiceTypeArgument::get_type() const { return this->type_; } +template<> EnumServiceArgType to_service_arg_type<bool>() { return SERVICE_ARG_TYPE_BOOL; } +template<> EnumServiceArgType to_service_arg_type<int>() { return SERVICE_ARG_TYPE_INT; } +template<> EnumServiceArgType to_service_arg_type<float>() { return SERVICE_ARG_TYPE_FLOAT; } +template<> EnumServiceArgType to_service_arg_type<std::string>() { return SERVICE_ARG_TYPE_STRING; } +template<> EnumServiceArgType to_service_arg_type<std::vector<bool>>() { return SERVICE_ARG_TYPE_BOOL_ARRAY; } +template<> EnumServiceArgType to_service_arg_type<std::vector<int>>() { return SERVICE_ARG_TYPE_INT_ARRAY; } +template<> EnumServiceArgType to_service_arg_type<std::vector<float>>() { return SERVICE_ARG_TYPE_FLOAT_ARRAY; } +template<> EnumServiceArgType to_service_arg_type<std::vector<std::string>>() { return SERVICE_ARG_TYPE_STRING_ARRAY; } } // namespace api } // namespace esphome diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 92e7a2ca0e..3b99d426a9 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -2,124 +2,71 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" -#include "api_message.h" +#include "api_pb2.h" namespace esphome { namespace api { -enum ServiceArgType { - SERVICE_ARG_TYPE_BOOL = 0, - SERVICE_ARG_TYPE_INT = 1, - SERVICE_ARG_TYPE_FLOAT = 2, - SERVICE_ARG_TYPE_STRING = 3, -}; - -class ServiceTypeArgument { - public: - ServiceTypeArgument(const std::string &name, ServiceArgType type); - const std::string &get_name() const; - ServiceArgType get_type() const; - - protected: - std::string name_; - ServiceArgType type_; -}; - -class ExecuteServiceArgument : public APIMessage { - public: - APIMessageType message_type() const override; - template<typename T> T get_value(); - - bool decode_varint(uint32_t field_id, uint32_t value) override; - bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override; - bool decode_32bit(uint32_t field_id, uint32_t value) override; - - protected: - bool value_bool_{false}; - int value_int_{0}; - float value_float_{0.0f}; - std::string value_string_{}; -}; - -class ExecuteServiceRequest : public APIMessage { - public: - bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override; - bool decode_32bit(uint32_t field_id, uint32_t value) override; - APIMessageType message_type() const override; - - uint32_t get_key() const; - const std::vector<ExecuteServiceArgument> &get_args() const; - - protected: - uint32_t key_; - std::vector<ExecuteServiceArgument> args_; -}; - class UserServiceDescriptor { public: - virtual void encode_list_service_response(APIBuffer &buffer) = 0; + virtual ListEntitiesServicesResponse encode_list_service_response() = 0; virtual bool execute_service(const ExecuteServiceRequest &req) = 0; }; -template<typename... Ts> class UserService : public UserServiceDescriptor, public Trigger<Ts...> { +template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg); + +template<typename T> EnumServiceArgType to_service_arg_type(); + +template<typename... Ts> class UserServiceBase : public UserServiceDescriptor { public: - UserService(const std::string &name, const std::array<ServiceTypeArgument, sizeof...(Ts)> &args); + UserServiceBase(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names) + : name_(name), arg_names_(arg_names) { + this->key_ = fnv1_hash(this->name_); + } - void encode_list_service_response(APIBuffer &buffer) override; + ListEntitiesServicesResponse encode_list_service_response() override { + ListEntitiesServicesResponse msg; + msg.name = this->name_; + msg.key = this->key_; + std::array<EnumServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...}; + for (int i = 0; i < sizeof...(Ts); i++) { + ListEntitiesServicesArgument arg; + arg.type = arg_types[i]; + arg.name = this->arg_names_[i]; + msg.args.push_back(arg); + } + return msg; + } - bool execute_service(const ExecuteServiceRequest &req) override; + bool execute_service(const ExecuteServiceRequest &req) override { + if (req.key != this->key_) + return false; + if (req.args.size() != this->arg_names_.size()) + return false; + this->execute_(req.args, typename gens<sizeof...(Ts)>::type()); + return true; + } protected: - template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...>); + virtual void execute(Ts... x) = 0; + template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...>) { + this->execute((get_execute_arg_value<Ts>(args[S]))...); + } std::string name_; uint32_t key_{0}; - std::array<ServiceTypeArgument, sizeof...(Ts)> args_; + std::array<std::string, sizeof...(Ts)> arg_names_; }; -template<typename... Ts> -template<int... S> -void UserService<Ts...>::execute_(std::vector<ExecuteServiceArgument> args, seq<S...>) { - this->trigger((args[S].get_value<Ts>())...); -} -template<typename... Ts> void UserService<Ts...>::encode_list_service_response(APIBuffer &buffer) { - // string name = 1; - buffer.encode_string(1, this->name_); - // fixed32 key = 2; - buffer.encode_fixed32(2, this->key_); +template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> { + public: + UserServiceTrigger(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names) + : UserServiceBase<Ts...>(name, arg_names) {} - // repeated ListServicesArgument args = 3; - for (auto &arg : this->args_) { - auto nested = buffer.begin_nested(3); - // string name = 1; - buffer.encode_string(1, arg.get_name()); - // Type type = 2; - buffer.encode_int32(2, arg.get_type()); - buffer.end_nested(nested); - } -} -template<typename... Ts> bool UserService<Ts...>::execute_service(const ExecuteServiceRequest &req) { - if (req.get_key() != this->key_) - return false; - - if (req.get_args().size() != this->args_.size()) { - return false; - } - - this->execute_(req.get_args(), typename gens<sizeof...(Ts)>::type()); - return true; -} -template<typename... Ts> -UserService<Ts...>::UserService(const std::string &name, const std::array<ServiceTypeArgument, sizeof...(Ts)> &args) - : name_(name), args_(args) { - this->key_ = fnv1_hash(this->name_); -} - -template<> bool ExecuteServiceArgument::get_value<bool>(); -template<> int ExecuteServiceArgument::get_value<int>(); -template<> float ExecuteServiceArgument::get_value<float>(); -template<> std::string ExecuteServiceArgument::get_value<std::string>(); + protected: + void execute(Ts... x) override { this->trigger(x...); } // NOLINT +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/util.cpp b/esphome/components/api/util.cpp index 9b8f91ea7d..f929db5d6a 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/components/api/util.cpp @@ -7,166 +7,6 @@ namespace esphome { namespace api { -APIBuffer::APIBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {} -size_t APIBuffer::get_length() const { return this->buffer_->size(); } -void APIBuffer::write(uint8_t value) { this->buffer_->push_back(value); } -void APIBuffer::encode_uint32(uint32_t field, uint32_t value, bool force) { - if (value == 0 && !force) - return; - - this->encode_field_raw(field, 0); - this->encode_varint_raw(value); -} -void APIBuffer::encode_int32(uint32_t field, int32_t value, bool force) { - this->encode_uint32(field, static_cast<uint32_t>(value), force); -} -void APIBuffer::encode_bool(uint32_t field, bool value, bool force) { - if (!value && !force) - return; - - this->encode_field_raw(field, 0); - this->write(0x01); -} -void APIBuffer::encode_string(uint32_t field, const std::string &value) { - this->encode_string(field, value.data(), value.size()); -} -void APIBuffer::encode_bytes(uint32_t field, const uint8_t *data, size_t len) { - this->encode_string(field, reinterpret_cast<const char *>(data), len); -} -void APIBuffer::encode_string(uint32_t field, const char *string, size_t len) { - if (len == 0) - return; - - this->encode_field_raw(field, 2); - this->encode_varint_raw(len); - const uint8_t *data = reinterpret_cast<const uint8_t *>(string); - for (size_t i = 0; i < len; i++) { - this->write(data[i]); - } -} -void APIBuffer::encode_fixed32(uint32_t field, uint32_t value, bool force) { - if (value == 0 && !force) - return; - - this->encode_field_raw(field, 5); - this->write((value >> 0) & 0xFF); - this->write((value >> 8) & 0xFF); - this->write((value >> 16) & 0xFF); - this->write((value >> 24) & 0xFF); -} -void APIBuffer::encode_float(uint32_t field, float value, bool force) { - if (value == 0.0f && !force) - return; - - union { - float value_f; - uint32_t value_raw; - } val; - val.value_f = value; - this->encode_fixed32(field, val.value_raw); -} -void APIBuffer::encode_field_raw(uint32_t field, uint32_t type) { - uint32_t val = (field << 3) | (type & 0b111); - this->encode_varint_raw(val); -} -void APIBuffer::encode_varint_raw(uint32_t value) { - if (value <= 0x7F) { - this->write(value); - return; - } - - while (value) { - uint8_t temp = value & 0x7F; - value >>= 7; - if (value) { - this->write(temp | 0x80); - } else { - this->write(temp); - } - } -} -void APIBuffer::encode_sint32(uint32_t field, int32_t value, bool force) { - if (value < 0) - this->encode_uint32(field, ~(uint32_t(value) << 1), force); - else - this->encode_uint32(field, uint32_t(value) << 1, force); -} -void APIBuffer::encode_nameable(Nameable *nameable) { - // string object_id = 1; - this->encode_string(1, nameable->get_object_id()); - // fixed32 key = 2; - this->encode_fixed32(2, nameable->get_object_id_hash()); - // string name = 3; - this->encode_string(3, nameable->get_name()); -} -size_t APIBuffer::begin_nested(uint32_t field) { - this->encode_field_raw(field, 2); - return this->buffer_->size(); -} -void APIBuffer::end_nested(size_t begin_index) { - const uint32_t nested_length = this->buffer_->size() - begin_index; - // add varint - std::vector<uint8_t> var; - uint32_t val = nested_length; - if (val <= 0x7F) { - var.push_back(val); - } else { - while (val) { - uint8_t temp = val & 0x7F; - val >>= 7; - if (val) { - var.push_back(temp | 0x80); - } else { - var.push_back(temp); - } - } - } - this->buffer_->insert(this->buffer_->begin() + begin_index, var.begin(), var.end()); -} - -optional<uint32_t> proto_decode_varuint32(const uint8_t *buf, size_t len, uint32_t *consumed) { - if (len == 0) - return {}; - - uint32_t result = 0; - uint8_t bitpos = 0; - - for (uint32_t i = 0; i < len; i++) { - uint8_t val = buf[i]; - result |= uint32_t(val & 0x7F) << bitpos; - bitpos += 7; - if ((val & 0x80) == 0) { - if (consumed != nullptr) { - *consumed = i + 1; - } - return result; - } - } - - return {}; -} - -std::string as_string(const uint8_t *value, size_t len) { - return std::string(reinterpret_cast<const char *>(value), len); -} - -int32_t as_sint32(uint32_t val) { - if (val & 1) - return uint32_t(~(val >> 1)); - else - return uint32_t(val >> 1); -} - -float as_float(uint32_t val) { - static_assert(sizeof(uint32_t) == sizeof(float), "float must be 32bit long"); - union { - uint32_t raw; - float value; - } x; - x.raw = val; - return x.value; -} - ComponentIterator::ComponentIterator(APIServer *server) : server_(server) {} void ComponentIterator::begin() { this->state_ = IteratorState::BEGIN; diff --git a/esphome/components/api/util.h b/esphome/components/api/util.h index 6478dec07a..5a29a48cbe 100644 --- a/esphome/components/api/util.h +++ b/esphome/components/api/util.h @@ -10,40 +10,6 @@ namespace esphome { namespace api { -class APIBuffer { - public: - APIBuffer(std::vector<uint8_t> *buffer); - - size_t get_length() const; - void write(uint8_t value); - - void encode_int32(uint32_t field, int32_t value, bool force = false); - void encode_uint32(uint32_t field, uint32_t value, bool force = false); - void encode_sint32(uint32_t field, int32_t value, bool force = false); - void encode_bool(uint32_t field, bool value, bool force = false); - void encode_string(uint32_t field, const std::string &value); - void encode_string(uint32_t field, const char *string, size_t len); - void encode_bytes(uint32_t field, const uint8_t *data, size_t len); - void encode_fixed32(uint32_t field, uint32_t value, bool force = false); - void encode_float(uint32_t field, float value, bool force = false); - void encode_nameable(Nameable *nameable); - - size_t begin_nested(uint32_t field); - void end_nested(size_t begin_index); - - void encode_field_raw(uint32_t field, uint32_t type); - void encode_varint_raw(uint32_t value); - - protected: - std::vector<uint8_t> *buffer_; -}; - -optional<uint32_t> proto_decode_varuint32(const uint8_t *buf, size_t len, uint32_t *consumed = nullptr); - -std::string as_string(const uint8_t *value, size_t len); -int32_t as_sint32(uint32_t val); -float as_float(uint32_t val); - class APIServer; class UserServiceDescriptor; diff --git a/esphome/components/as3935/__init__.py b/esphome/components/as3935/__init__.py new file mode 100644 index 0000000000..de25060623 --- /dev/null +++ b/esphome/components/as3935/__init__.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import CONF_INDOOR, CONF_WATCHDOG_THRESHOLD, \ + CONF_NOISE_LEVEL, CONF_SPIKE_REJECTION, CONF_LIGHTNING_THRESHOLD, \ + CONF_MASK_DISTURBER, CONF_DIV_RATIO, CONF_CAPACITANCE +from esphome.core import coroutine + +AUTO_LOAD = ['sensor', 'binary_sensor'] +MULTI_CONF = True + +CONF_AS3935_ID = 'as3935_id' + +as3935_ns = cg.esphome_ns.namespace('as3935') +AS3935 = as3935_ns.class_('AS3935Component', cg.Component) + +CONF_IRQ_PIN = 'irq_pin' +AS3935_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(AS3935), + cv.Required(CONF_IRQ_PIN): pins.gpio_input_pin_schema, + + cv.Optional(CONF_INDOOR, default=True): cv.boolean, + cv.Optional(CONF_NOISE_LEVEL, default=2): cv.int_range(min=1, max=7), + cv.Optional(CONF_WATCHDOG_THRESHOLD, default=2): cv.int_range(min=1, max=10), + cv.Optional(CONF_SPIKE_REJECTION, default=2): cv.int_range(min=1, max=11), + cv.Optional(CONF_LIGHTNING_THRESHOLD, default=1): cv.one_of(1, 5, 9, 16, int=True), + cv.Optional(CONF_MASK_DISTURBER, default=False): cv.boolean, + cv.Optional(CONF_DIV_RATIO, default=0): cv.one_of(0, 16, 22, 64, 128, int=True), + cv.Optional(CONF_CAPACITANCE, default=0): cv.int_range(min=0, max=15), +}) + + +@coroutine +def setup_as3935(var, config): + yield cg.register_component(var, config) + + irq_pin = yield cg.gpio_pin_expression(config[CONF_IRQ_PIN]) + cg.add(var.set_irq_pin(irq_pin)) + cg.add(var.set_indoor(config[CONF_INDOOR])) + cg.add(var.set_noise_level(config[CONF_NOISE_LEVEL])) + cg.add(var.set_watchdog_threshold(config[CONF_WATCHDOG_THRESHOLD])) + cg.add(var.set_spike_rejection(config[CONF_SPIKE_REJECTION])) + cg.add(var.set_lightning_threshold(config[CONF_LIGHTNING_THRESHOLD])) + cg.add(var.set_mask_disturber(config[CONF_MASK_DISTURBER])) + cg.add(var.set_div_ratio(config[CONF_DIV_RATIO])) + cg.add(var.set_capacitance(config[CONF_CAPACITANCE])) diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp new file mode 100644 index 0000000000..f8272e6036 --- /dev/null +++ b/esphome/components/as3935/as3935.cpp @@ -0,0 +1,223 @@ +#include "as3935.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace as3935 { + +static const char *TAG = "as3935"; + +void AS3935Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up AS3935..."); + + this->irq_pin_->setup(); + LOG_PIN(" IRQ Pin: ", this->irq_pin_); + + // Write properties to sensor + this->write_indoor(this->indoor_); + this->write_noise_level(this->noise_level_); + this->write_watchdog_threshold(this->watchdog_threshold_); + this->write_spike_rejection(this->spike_rejection_); + this->write_lightning_threshold(this->lightning_threshold_); + this->write_mask_disturber(this->mask_disturber_); + this->write_div_ratio(this->div_ratio_); + this->write_capacitance(this->capacitance_); +} + +void AS3935Component::dump_config() { + ESP_LOGCONFIG(TAG, "AS3935:"); + LOG_PIN(" Interrupt Pin: ", this->irq_pin_); +} + +float AS3935Component::get_setup_priority() const { return setup_priority::DATA; } + +void AS3935Component::loop() { + if (!this->irq_pin_->digital_read()) + return; + + uint8_t int_value = this->read_interrupt_register_(); + if (int_value == NOISE_INT) { + ESP_LOGI(TAG, "Noise was detected - try increasing the noise level value!"); + } else if (int_value == DISTURBER_INT) { + ESP_LOGI(TAG, "Disturber was detected - try increasing the spike rejection value!"); + } else if (int_value == LIGHTNING_INT) { + ESP_LOGI(TAG, "Lightning has been detected!"); + if (this->thunder_alert_binary_sensor_ != nullptr) + this->thunder_alert_binary_sensor_->publish_state(true); + uint8_t distance = this->get_distance_to_storm_(); + if (this->distance_sensor_ != nullptr) + this->distance_sensor_->publish_state(distance); + uint32_t energy = this->get_lightning_energy_(); + if (this->energy_sensor_ != nullptr) + this->energy_sensor_->publish_state(energy); + } + this->thunder_alert_binary_sensor_->publish_state(false); +} + +void AS3935Component::write_indoor(bool indoor) { + ESP_LOGV(TAG, "Setting indoor to %d", indoor); + if (indoor) + this->write_register(AFE_GAIN, GAIN_MASK, INDOOR, 1); + else + this->write_register(AFE_GAIN, GAIN_MASK, OUTDOOR, 1); +} +// REG0x01, bits[3:0], manufacturer default: 0010 (2). +// This setting determines the threshold for events that trigger the +// IRQ Pin. +void AS3935Component::write_watchdog_threshold(uint8_t watchdog_threshold) { + ESP_LOGV(TAG, "Setting watchdog sensitivity to %d", watchdog_threshold); + if ((watchdog_threshold < 1) || (watchdog_threshold > 10)) // 10 is the max sensitivity setting + return; + this->write_register(THRESHOLD, THRESH_MASK, watchdog_threshold, 0); +} + +// REG0x01, bits [6:4], manufacturer default: 010 (2). +// The noise floor level is compared to a known reference voltage. If this +// level is exceeded the chip will issue an interrupt to the IRQ pin, +// broadcasting that it can not operate properly due to noise (INT_NH). +// Check datasheet for specific noise level tolerances when setting this register. +void AS3935Component::write_noise_level(uint8_t noise_level) { + ESP_LOGV(TAG, "Setting noise level to %d", noise_level); + if ((noise_level < 1) || (noise_level > 7)) + return; + + this->write_register(THRESHOLD, NOISE_FLOOR_MASK, noise_level, 4); +} +// REG0x02, bits [3:0], manufacturer default: 0010 (2). +// This setting, like the watchdog threshold, can help determine between false +// events and actual lightning. The shape of the spike is analyzed during the +// chip's signal validation routine. Increasing this value increases robustness +// at the cost of sensitivity to distant events. +void AS3935Component::write_spike_rejection(uint8_t spike_rejection) { + ESP_LOGV(TAG, "Setting spike rejection to %d", spike_rejection); + if ((spike_rejection < 1) || (spike_rejection > 11)) + return; + + this->write_register(LIGHTNING_REG, SPIKE_MASK, spike_rejection, 0); +} +// REG0x02, bits [5:4], manufacturer default: 0 (single lightning strike). +// The number of lightning events before IRQ is set high. 15 minutes is The +// window of time before the number of detected lightning events is reset. +// The number of lightning strikes can be set to 1,5,9, or 16. +void AS3935Component::write_lightning_threshold(uint8_t lightning_threshold) { + ESP_LOGV(TAG, "Setting lightning threshold to %d", lightning_threshold); + switch (lightning_threshold) { + case 1: + this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 0, 4); // Demonstrative + break; + case 5: + this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 1, 4); + break; + case 9: + this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 1, 5); + break; + case 16: + this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 3, 4); + break; + default: + return; + } +} +// REG0x03, bit [5], manufacturer default: 0. +// This setting will return whether or not disturbers trigger the IRQ Pin. +void AS3935Component::write_mask_disturber(bool enabled) { + ESP_LOGV(TAG, "Setting mask disturber to %d", enabled); + if (enabled) { + this->write_register(INT_MASK_ANT, (1 << 5), 1, 5); + } else { + this->write_register(INT_MASK_ANT, (1 << 5), 0, 5); + } +} +// REG0x03, bit [7:6], manufacturer default: 0 (16 division ratio). +// The antenna is designed to resonate at 500kHz and so can be tuned with the +// following setting. The accuracy of the antenna must be within 3.5 percent of +// that value for proper signal validation and distance estimation. +void AS3935Component::write_div_ratio(uint8_t div_ratio) { + ESP_LOGV(TAG, "Setting div ratio to %d", div_ratio); + switch (div_ratio) { + case 16: + this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 0, 6); + break; + case 22: + this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 1, 6); + break; + case 64: + this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 1, 7); + break; + case 128: + this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 3, 6); + break; + default: + return; + } +} +// REG0x08, bits [3:0], manufacturer default: 0. +// This setting will add capacitance to the series RLC antenna on the product +// to help tune its resonance. The datasheet specifies being within 3.5 percent +// of 500kHz to get optimal lightning detection and distance sensing. +// It's possible to add up to 120pF in steps of 8pF to the antenna. +void AS3935Component::write_capacitance(uint8_t capacitance) { + ESP_LOGV(TAG, "Setting tune cap to %d pF", capacitance * 8); + this->write_register(FREQ_DISP_IRQ, CAP_MASK, capacitance, 0); +} + +// REG0x03, bits [3:0], manufacturer default: 0. +// When there is an event that exceeds the watchdog threshold, the register is written +// with the type of event. This consists of two messages: INT_D (disturber detected) and +// INT_L (Lightning detected). A third interrupt INT_NH (noise level too HIGH) +// indicates that the noise level has been exceeded and will persist until the +// noise has ended. Events are active HIGH. There is a one second window of time to +// read the interrupt register after lightning is detected, and 1.5 after +// disturber. +uint8_t AS3935Component::read_interrupt_register_() { + // A 2ms delay is added to allow for the memory register to be populated + // after the interrupt pin goes HIGH. See "Interrupt Management" in + // datasheet. + ESP_LOGV(TAG, "Calling read_interrupt_register_"); + delay(2); + return this->read_register_(INT_MASK_ANT, INT_MASK); +} + +// REG0x02, bit [6], manufacturer default: 1. +// This register clears the number of lightning strikes that has been read in +// the last 15 minute block. +void AS3935Component::clear_statistics_() { + // Write high, then low, then high to clear. + ESP_LOGV(TAG, "Calling clear_statistics_"); + this->write_register(LIGHTNING_REG, (1 << 6), 1, 6); + this->write_register(LIGHTNING_REG, (1 << 6), 0, 6); + this->write_register(LIGHTNING_REG, (1 << 6), 1, 6); +} + +// REG0x07, bit [5:0], manufacturer default: 0. +// This register holds the distance to the front of the storm and not the +// distance to a lightning strike. +uint8_t AS3935Component::get_distance_to_storm_() { + ESP_LOGV(TAG, "Calling get_distance_to_storm_"); + return this->read_register_(DISTANCE, DISTANCE_MASK); +} + +uint32_t AS3935Component::get_lightning_energy_() { + ESP_LOGV(TAG, "Calling get_lightning_energy_"); + uint32_t pure_light = 0; // Variable for lightning energy which is just a pure number. + uint32_t temp = 0; + // Temp variable for lightning energy. + temp = this->read_register_(ENERGY_LIGHT_MMSB, ENERGY_MASK); + // Temporary Value is large enough to handle a shift of 16 bits. + pure_light = temp << 16; + temp = this->read_register(ENERGY_LIGHT_MSB); + // Temporary value is large enough to handle a shift of 8 bits. + pure_light |= temp << 8; + // No shift here, directly OR'ed into pure_light variable. + temp = this->read_register(ENERGY_LIGHT_LSB); + pure_light |= temp; + return pure_light; +} + +uint8_t AS3935Component::read_register_(uint8_t reg, uint8_t mask) { + uint8_t value = this->read_register(reg); + value &= (~mask); + return value; +} + +} // namespace as3935 +} // namespace esphome diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h new file mode 100644 index 0000000000..d0e53e7832 --- /dev/null +++ b/esphome/components/as3935/as3935.h @@ -0,0 +1,110 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace as3935 { + +enum AS3935RegisterNames { + AFE_GAIN = 0x00, + THRESHOLD, + LIGHTNING_REG, + INT_MASK_ANT, + ENERGY_LIGHT_LSB, + ENERGY_LIGHT_MSB, + ENERGY_LIGHT_MMSB, + DISTANCE, + FREQ_DISP_IRQ, + CALIB_TRCO = 0x3A, + CALIB_SRCO = 0x3B, + DEFAULT_RESET = 0x3C, + CALIB_RCO = 0x3D +}; + +enum AS3935RegisterMasks { + GAIN_MASK = 0x3E, + SPIKE_MASK = 0xF, + IO_MASK = 0xC1, + DISTANCE_MASK = 0xC0, + INT_MASK = 0xF0, + THRESH_MASK = 0x0F, + R_SPIKE_MASK = 0xF0, + ENERGY_MASK = 0xF0, + CAP_MASK = 0xF0, + LIGHT_MASK = 0xCF, + DISTURB_MASK = 0xDF, + NOISE_FLOOR_MASK = 0x70, + OSC_MASK = 0xE0, + CALIB_MASK = 0x7F, + DIV_MASK = 0x3F +}; + +enum AS3935Values { + AS3935_ADDR = 0x03, + INDOOR = 0x12, + OUTDOOR = 0xE, + LIGHTNING_INT = 0x08, + DISTURBER_INT = 0x04, + NOISE_INT = 0x01 +}; + +class AS3935Component : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + void set_irq_pin(GPIOPin *irq_pin) { irq_pin_ = irq_pin; } + void set_distance_sensor(sensor::Sensor *distance_sensor) { distance_sensor_ = distance_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } + void set_thunder_alert_binary_sensor(binary_sensor::BinarySensor *thunder_alert_binary_sensor) { + thunder_alert_binary_sensor_ = thunder_alert_binary_sensor; + } + void set_indoor(bool indoor) { indoor_ = indoor; } + void write_indoor(bool indoor); + void set_noise_level(uint8_t noise_level) { noise_level_ = noise_level; } + void write_noise_level(uint8_t noise_level); + void set_watchdog_threshold(uint8_t watchdog_threshold) { watchdog_threshold_ = watchdog_threshold; } + void write_watchdog_threshold(uint8_t watchdog_threshold); + void set_spike_rejection(uint8_t spike_rejection) { spike_rejection_ = spike_rejection; } + void write_spike_rejection(uint8_t write_spike_rejection); + void set_lightning_threshold(uint8_t lightning_threshold) { lightning_threshold_ = lightning_threshold; } + void write_lightning_threshold(uint8_t lightning_threshold); + void set_mask_disturber(bool mask_disturber) { mask_disturber_ = mask_disturber; } + void write_mask_disturber(bool enabled); + void set_div_ratio(uint8_t div_ratio) { div_ratio_ = div_ratio; } + void write_div_ratio(uint8_t div_ratio); + void set_capacitance(uint8_t capacitance) { capacitance_ = capacitance; } + void write_capacitance(uint8_t capacitance); + + protected: + uint8_t read_interrupt_register_(); + void clear_statistics_(); + uint8_t get_distance_to_storm_(); + uint32_t get_lightning_energy_(); + + virtual uint8_t read_register(uint8_t reg) = 0; + uint8_t read_register_(uint8_t reg, uint8_t mask); + + virtual void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) = 0; + + sensor::Sensor *distance_sensor_; + sensor::Sensor *energy_sensor_; + binary_sensor::BinarySensor *thunder_alert_binary_sensor_; + GPIOPin *irq_pin_; + + bool indoor_; + uint8_t noise_level_; + uint8_t watchdog_threshold_; + uint8_t spike_rejection_; + uint8_t lightning_threshold_; + bool mask_disturber_; + uint8_t div_ratio_; + uint8_t capacitance_; +}; + +} // namespace as3935 +} // namespace esphome diff --git a/esphome/components/as3935/binary_sensor.py b/esphome/components/as3935/binary_sensor.py new file mode 100644 index 0000000000..3748c3484a --- /dev/null +++ b/esphome/components/as3935/binary_sensor.py @@ -0,0 +1,16 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from . import AS3935, CONF_AS3935_ID + +DEPENDENCIES = ['as3935'] + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935), +}) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_AS3935_ID]) + var = yield binary_sensor.new_binary_sensor(config) + cg.add(hub.set_thunder_alert_binary_sensor(var)) diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py new file mode 100644 index 0000000000..3374ada6a8 --- /dev/null +++ b/esphome/components/as3935/sensor.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_DISTANCE, CONF_LIGHTNING_ENERGY, \ + UNIT_KILOMETER, UNIT_EMPTY, ICON_SIGNAL_DISTANCE_VARIANT, ICON_FLASH +from . import AS3935, CONF_AS3935_ID + +DEPENDENCIES = ['as3935'] + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935), + cv.Optional(CONF_DISTANCE): + sensor.sensor_schema(UNIT_KILOMETER, ICON_SIGNAL_DISTANCE_VARIANT, 1), + cv.Optional(CONF_LIGHTNING_ENERGY): + sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 1), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_AS3935_ID]) + + if CONF_DISTANCE in config: + conf = config[CONF_DISTANCE] + distance_sensor = yield sensor.new_sensor(conf) + cg.add(hub.set_distance_sensor(distance_sensor)) + + if CONF_LIGHTNING_ENERGY in config: + conf = config[CONF_LIGHTNING_ENERGY] + lightning_energy_sensor = yield sensor.new_sensor(conf) + cg.add(hub.set_distance_sensor(lightning_energy_sensor)) diff --git a/esphome/components/as3935_i2c/__init__.py b/esphome/components/as3935_i2c/__init__.py new file mode 100644 index 0000000000..e22937ab81 --- /dev/null +++ b/esphome/components/as3935_i2c/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import as3935, i2c +from esphome.const import CONF_ID + +AUTO_LOAD = ['as3935'] +DEPENDENCIES = ['i2c'] + +as3935_i2c_ns = cg.esphome_ns.namespace('as3935_i2c') +I2CAS3935 = as3935_i2c_ns.class_('I2CAS3935Component', as3935.AS3935, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(I2CAS3935), +}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x03))) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield as3935.setup_as3935(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/as3935_i2c/as3935_i2c.cpp b/esphome/components/as3935_i2c/as3935_i2c.cpp new file mode 100644 index 0000000000..a522116815 --- /dev/null +++ b/esphome/components/as3935_i2c/as3935_i2c.cpp @@ -0,0 +1,40 @@ +#include "as3935_i2c.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace as3935_i2c { + +static const char *TAG = "as3935_i2c"; + +void I2CAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_pos) { + uint8_t write_reg; + if (!this->read_byte(reg, &write_reg)) { + this->mark_failed(); + ESP_LOGW(TAG, "read_byte failed - increase log level for more details!"); + return; + } + + write_reg &= (~mask); + write_reg |= (bits << start_pos); + + if (!this->write_byte(reg, write_reg)) { + ESP_LOGW(TAG, "write_byte failed - increase log level for more details!"); + return; + } +} + +uint8_t I2CAS3935Component::read_register(uint8_t reg) { + uint8_t value; + if (!this->read_byte(reg, &value, 2)) { + ESP_LOGW(TAG, "Read failed!"); + return 0; + } + return value; +} +void I2CAS3935Component::dump_config() { + AS3935Component::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace as3935_i2c +} // namespace esphome diff --git a/esphome/components/as3935_i2c/as3935_i2c.h b/esphome/components/as3935_i2c/as3935_i2c.h new file mode 100644 index 0000000000..1d16397bdf --- /dev/null +++ b/esphome/components/as3935_i2c/as3935_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/as3935/as3935.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace as3935_i2c { + +class I2CAS3935Component : public as3935::AS3935Component, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) override; + uint8_t read_register(uint8_t reg) override; +}; + +} // namespace as3935_i2c +} // namespace esphome diff --git a/esphome/components/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py new file mode 100644 index 0000000000..fa27c2b0f5 --- /dev/null +++ b/esphome/components/as3935_spi/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import as3935, spi +from esphome.const import CONF_ID + +AUTO_LOAD = ['as3935'] +DEPENDENCIES = ['spi'] + +as3935_spi_ns = cg.esphome_ns.namespace('as3935_spi') +SPIAS3935 = as3935_spi_ns.class_('SPIAS3935Component', as3935.AS3935, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPIAS3935) +}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield as3935.setup_as3935(var, config) + yield spi.register_spi_device(var, config) diff --git a/esphome/components/as3935_spi/as3935_spi.cpp b/esphome/components/as3935_spi/as3935_spi.cpp new file mode 100644 index 0000000000..73752a8ee0 --- /dev/null +++ b/esphome/components/as3935_spi/as3935_spi.cpp @@ -0,0 +1,48 @@ +#include "as3935_spi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace as3935_spi { + +static const char *TAG = "as3935_spi"; + +void SPIAS3935Component::setup() { + ESP_LOGI(TAG, "SPIAS3935Component setup started!"); + this->spi_setup(); + ESP_LOGI(TAG, "SPI setup finished!"); + AS3935Component::setup(); +} + +void SPIAS3935Component::dump_config() { + AS3935Component::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +void SPIAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_pos) { + uint8_t write_reg = this->read_register(reg); + + write_reg &= (~mask); + write_reg |= (bits << start_pos); + + this->enable(); + this->write_byte(reg); + this->write_byte(write_reg); + this->disable(); +} + +uint8_t SPIAS3935Component::read_register(uint8_t reg) { + uint8_t value = 0; + this->enable(); + this->write_byte(reg |= SPI_READ_M); + value = this->read_byte(); + // According to datsheet, the chip select must be written HIGH, LOW, HIGH + // to correctly end the READ command. + this->cs_->digital_write(true); + this->cs_->digital_write(false); + this->disable(); + ESP_LOGV(TAG, "read_register_: %d", value); + return value; +} + +} // namespace as3935_spi +} // namespace esphome diff --git a/esphome/components/as3935_spi/as3935_spi.h b/esphome/components/as3935_spi/as3935_spi.h new file mode 100644 index 0000000000..073f5c09a4 --- /dev/null +++ b/esphome/components/as3935_spi/as3935_spi.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/as3935/as3935.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace as3935_spi { + +enum AS3935RegisterMasks { SPI_READ_M = 0x40 }; + +class SPIAS3935Component : public as3935::AS3935Component, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, + spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> { + public: + void setup() override; + void dump_config() override; + + protected: + void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) override; + uint8_t read_register(uint8_t reg) override; +}; + +} // namespace as3935_spi +} // namespace esphome diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py new file mode 100644 index 0000000000..4cbd5358ce --- /dev/null +++ b/esphome/components/async_tcp/__init__.py @@ -0,0 +1,23 @@ +# Dummy integration to allow relying on AsyncTCP +import esphome.codegen as cg +from esphome.const import ARDUINO_VERSION_ESP32_1_0_0, ARDUINO_VERSION_ESP32_1_0_1, \ + ARDUINO_VERSION_ESP32_1_0_2 +from esphome.core import CORE, coroutine_with_priority + + +@coroutine_with_priority(200.0) +def to_code(config): + if CORE.is_esp32: + # https://github.com/me-no-dev/AsyncTCP/blob/master/library.json + versions_requiring_older_asynctcp = [ + ARDUINO_VERSION_ESP32_1_0_0, + ARDUINO_VERSION_ESP32_1_0_1, + ARDUINO_VERSION_ESP32_1_0_2, + ] + if CORE.arduino_version in versions_requiring_older_asynctcp: + cg.add_library('AsyncTCP', '1.0.3') + else: + cg.add_library('AsyncTCP', '1.1.1') + elif CORE.is_esp8266: + # https://github.com/OttoWinter/ESPAsyncTCP + cg.add_library('ESPAsyncTCP-esphome', '1.2.2') diff --git a/esphome/components/atm90e32/__init__.py b/esphome/components/atm90e32/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp new file mode 100644 index 0000000000..283319994e --- /dev/null +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -0,0 +1,188 @@ +#include "atm90e32.h" +#include "atm90e32_reg.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace atm90e32 { + +static const char *TAG = "atm90e32"; + +void ATM90E32Component::update() { + if (this->read16_(ATM90E32_REGISTER_METEREN) != 1) { + this->status_set_warning(); + return; + } + + if (this->phase_[0].voltage_sensor_ != nullptr) { + this->phase_[0].voltage_sensor_->publish_state(this->get_line_voltage_a_()); + } + if (this->phase_[1].voltage_sensor_ != nullptr) { + this->phase_[1].voltage_sensor_->publish_state(this->get_line_voltage_b_()); + } + if (this->phase_[2].voltage_sensor_ != nullptr) { + this->phase_[2].voltage_sensor_->publish_state(this->get_line_voltage_c_()); + } + if (this->phase_[0].current_sensor_ != nullptr) { + this->phase_[0].current_sensor_->publish_state(this->get_line_current_a_()); + } + if (this->phase_[1].current_sensor_ != nullptr) { + this->phase_[1].current_sensor_->publish_state(this->get_line_current_b_()); + } + if (this->phase_[2].current_sensor_ != nullptr) { + this->phase_[2].current_sensor_->publish_state(this->get_line_current_c_()); + } + if (this->phase_[0].power_sensor_ != nullptr) { + this->phase_[0].power_sensor_->publish_state(this->get_active_power_a_()); + } + if (this->phase_[1].power_sensor_ != nullptr) { + this->phase_[1].power_sensor_->publish_state(this->get_active_power_b_()); + } + if (this->phase_[2].power_sensor_ != nullptr) { + this->phase_[2].power_sensor_->publish_state(this->get_active_power_c_()); + } + if (this->freq_sensor_ != nullptr) { + this->freq_sensor_->publish_state(this->get_frequency_()); + } + this->status_clear_warning(); +} + +void ATM90E32Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ATM90E32Component..."); + this->spi_setup(); + + uint16_t mmode0 = 0x185; + if (line_freq_ == 60) { + mmode0 |= 1 << 12; + } + + this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access + this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering + if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x0001) { + ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings"); + this->mark_failed(); + return; + } + + this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0x0A55); // ZX2, ZX1, ZX0 pin config + this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) + this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels + this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x0AFC); // Active Startup Power Threshold = 50% + this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x0AEC); // Reactive Startup Power Threshold = 50% + this->write16_(ATM90E32_REGISTER_PPHASETH, 0x00BC); // Active Phase Threshold = 10% + this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[0].volt_gain_); // A Voltage rms gain + this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[0].ct_gain_); // A line current gain + this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[1].volt_gain_); // B Voltage rms gain + this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[1].ct_gain_); // B line current gain + this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[2].volt_gain_); // C Voltage rms gain + this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[2].ct_gain_); // C line current gain + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration +} + +void ATM90E32Component::dump_config() { + ESP_LOGCONFIG("", "ATM90E32:"); + LOG_PIN(" CS Pin: ", this->cs_); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with ATM90E32 failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Voltage A", this->phase_[0].voltage_sensor_); + LOG_SENSOR(" ", "Current A", this->phase_[0].current_sensor_); + LOG_SENSOR(" ", "Power A", this->phase_[0].power_sensor_); + LOG_SENSOR(" ", "Voltage B", this->phase_[1].voltage_sensor_); + LOG_SENSOR(" ", "Current B", this->phase_[1].current_sensor_); + LOG_SENSOR(" ", "Power B", this->phase_[1].power_sensor_); + LOG_SENSOR(" ", "Voltage C", this->phase_[2].voltage_sensor_); + LOG_SENSOR(" ", "Current C", this->phase_[2].current_sensor_); + LOG_SENSOR(" ", "Power C", this->phase_[2].power_sensor_); + LOG_SENSOR(" ", "Frequency", this->freq_sensor_) +} +float ATM90E32Component::get_setup_priority() const { return setup_priority::DATA; } + +uint16_t ATM90E32Component::read16_(uint16_t a_register) { + uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03); + uint8_t addrl = (a_register & 0xFF); + uint8_t data[2]; + uint16_t output; + + this->enable(); + delayMicroseconds(10); + this->write_byte(addrh); + this->write_byte(addrl); + delayMicroseconds(4); + this->read_array(data, 2); + this->disable(); + + output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); + ESP_LOGVV(TAG, "read16_ 0x%04X output 0x%04X", a_register, output); + return output; +} + +int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) { + uint16_t val_h = this->read16_(addr_h); + uint16_t val_l = this->read16_(addr_l); + int32_t val = (val_h << 16) | val_l; + + ESP_LOGVV(TAG, "read32_ addr_h 0x%04X val_h 0x%04X addr_l 0x%04X val_l 0x%04X = %d", addr_h, val_h, addr_l, val_l, + val); + + return val; +} + +void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) { + uint8_t addrh = (a_register >> 8) & 0x03; + uint8_t addrl = (a_register & 0xFF); + + ESP_LOGVV(TAG, "write16_ 0x%04X val 0x%04X", a_register, val); + this->enable(); + delayMicroseconds(10); + this->write_byte(addrh); + this->write_byte(addrl); + delayMicroseconds(4); + this->write_byte((val >> 8) & 0xff); + this->write_byte(val & 0xFF); + this->disable(); +} + +float ATM90E32Component::get_line_voltage_a_() { + uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSA); + return (float) voltage / 100; +} +float ATM90E32Component::get_line_voltage_b_() { + uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSB); + return (float) voltage / 100; +} +float ATM90E32Component::get_line_voltage_c_() { + uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMSC); + return (float) voltage / 100; +} +float ATM90E32Component::get_line_current_a_() { + uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSA); + return (float) current / 1000; +} +float ATM90E32Component::get_line_current_b_() { + uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSB); + return (float) current / 1000; +} +float ATM90E32Component::get_line_current_c_() { + uint16_t current = this->read16_(ATM90E32_REGISTER_IRMSC); + return (float) current / 1000; +} +float ATM90E32Component::get_active_power_a_() { + int val = this->read32_(ATM90E32_REGISTER_PMEANA, ATM90E32_REGISTER_PMEANALSB); + return val * 0.00032f; +} +float ATM90E32Component::get_active_power_b_() { + int val = this->read32_(ATM90E32_REGISTER_PMEANB, ATM90E32_REGISTER_PMEANBLSB); + return val * 0.00032f; +} +float ATM90E32Component::get_active_power_c_() { + int val = this->read32_(ATM90E32_REGISTER_PMEANC, ATM90E32_REGISTER_PMEANCLSB); + return val * 0.00032f; +} +float ATM90E32Component::get_frequency_() { + uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ); + return (float) freq / 100; +} +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h new file mode 100644 index 0000000000..4dd2bd5784 --- /dev/null +++ b/esphome/components/atm90e32/atm90e32.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace atm90e32 { + +class ATM90E32Component : public PollingComponent, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, + spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_voltage_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].voltage_sensor_ = obj; } + void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } + void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } + void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].volt_gain_ = gain; } + void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } + + void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } + void set_line_freq(int freq) { line_freq_ = freq; } + void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } + + protected: + uint16_t read16_(uint16_t a_register); + int read32_(uint16_t addr_h, uint16_t addr_l); + void write16_(uint16_t a_register, uint16_t val); + + float get_line_voltage_a_(); + float get_line_voltage_b_(); + float get_line_voltage_c_(); + float get_line_current_a_(); + float get_line_current_b_(); + float get_line_current_c_(); + float get_active_power_a_(); + float get_active_power_b_(); + float get_active_power_c_(); + float get_frequency_(); + + struct ATM90E32Phase { + uint16_t volt_gain_{41820}; + uint16_t ct_gain_{25498}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + } phase_[3]; + sensor::Sensor *freq_sensor_{nullptr}; + uint16_t pga_gain_{0x15}; + int line_freq_{60}; +}; + +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h new file mode 100644 index 0000000000..ca3715a2a8 --- /dev/null +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -0,0 +1,253 @@ +#pragma once + +namespace esphome { +namespace atm90e32 { + +/* STATUS REGISTERS */ +static const uint16_t ATM90E32_REGISTER_METEREN = 0x00; // Metering Enable +static const uint16_t ATM90E32_REGISTER_CHANNELMAPI = 0x01; // Current Channel Mapping Configuration +static const uint16_t ATM90E32_REGISTER_CHANNELMAPU = 0x02; // Voltage Channel Mapping Configuration +static const uint16_t ATM90E32_REGISTER_SAGPEAKDETCFG = 0x05; // Sag and Peak Detector Period Configuration +static const uint16_t ATM90E32_REGISTER_OVTH = 0x06; // Over Voltage Threshold +static const uint16_t ATM90E32_REGISTER_ZXCONFIG = 0x07; // Zero-Crossing Config +static const uint16_t ATM90E32_REGISTER_SAGTH = 0x08; // Voltage Sag Th +static const uint16_t ATM90E32_REGISTER_PHASELOSSTH = 0x09; // Voltage Phase Losing Th +static const uint16_t ATM90E32_REGISTER_INWARNTH = 0x0A; // Neutral Current (Calculated) Warning Threshold +static const uint16_t ATM90E32_REGISTER_OITH = 0x0B; // Over Current Threshold +static const uint16_t ATM90E32_REGISTER_FREQLOTH = 0x0C; // Low Threshold for Frequency Detection +static const uint16_t ATM90E32_REGISTER_FREQHITH = 0x0D; // High Threshold for Frequency Detection +static const uint16_t ATM90E32_REGISTER_PMPWRCTRL = 0x0E; // Partial Measurement Mode Power Control +static const uint16_t ATM90E32_REGISTER_IRQ0MERGECFG = 0x0F; // IRQ0 Merge Configuration + +/* EMM STATUS REGISTERS */ +static const uint16_t ATM90E32_REGISTER_SOFTRESET = 0x70; // Software Reset +static const uint16_t ATM90E32_REGISTER_EMMSTATE0 = 0x71; // EMM State 0 +static const uint16_t ATM90E32_REGISTER_EMMSTATE1 = 0x72; // EMM State 1 +static const uint16_t ATM90E32_REGISTER_EMMINTSTATE0 = 0x73; // EMM Interrupt Status 0 +static const uint16_t ATM90E32_REGISTER_EMMINTSTATE1 = 0x74; // EMM Interrupt Status 1 +static const uint16_t ATM90E32_REGISTER_EMMINTEN0 = 0x75; // EMM Interrupt Enable 0 +static const uint16_t ATM90E32_REGISTER_EMMINTEN1 = 0x76; // EMM Interrupt Enable 1 +static const uint16_t ATM90E32_REGISTER_LASTSPIDATA = 0x78; // Last Read/Write SPI Value +static const uint16_t ATM90E32_REGISTER_CRCERRSTATUS = 0x79; // CRC Error Status +static const uint16_t ATM90E32_REGISTER_CRCDIGEST = 0x7A; // CRC Digest +static const uint16_t ATM90E32_REGISTER_CFGREGACCEN = 0x7F; // Configure Register Access Enable +static const uint16_t ATM90E32_STATUS_S0_OIPHASEAST = 1 << 15; // Over current on phase A +static const uint16_t ATM90E32_STATUS_S0_OIPHASEBST = 1 << 14; // Over current on phase B +static const uint16_t ATM90E32_STATUS_S0_OIPHASECST = 1 << 13; // Over current on phase C +static const uint16_t ATM90E32_STATUS_S0_OVPHASEAST = 1 << 12; // Over voltage on phase A +static const uint16_t ATM90E32_STATUS_S0_OVPHASEBST = 1 << 11; // Over voltage on phase B +static const uint16_t ATM90E32_STATUS_S0_OVPHASECST = 1 << 10; // Over voltage on phase C +static const uint16_t ATM90E32_STATUS_S0_UREVWNST = 1 << 9; // Voltage Phase Sequence Error status +static const uint16_t ATM90E32_STATUS_S0_IREVWNST = 1 << 8; // Current Phase Sequence Error status +static const uint16_t ATM90E32_STATUS_S0_INOV0ST = 1 << 7; // Calculated N line current greater tha INWarnTh reg +static const uint16_t ATM90E32_STATUS_S0_TQNOLOADST = 1 << 6; // All phase sum reactive power no-load condition status +static const uint16_t ATM90E32_STATUS_S0_TPNOLOADST = 1 << 5; // All phase sum active power no-load condition status +static const uint16_t ATM90E32_STATUS_S0_TASNOLOADST = 1 << 4; // All phase sum apparent power no-load status +static const uint16_t ATM90E32_STATUS_S0_CF1REVST = 1 << 3; // Energy for CF1 Forward/Reverse status +static const uint16_t ATM90E32_STATUS_S0_CF2REVST = 1 << 2; // Energy for CF2 Forward/Reverse status +static const uint16_t ATM90E32_STATUS_S0_CF3REVST = 1 << 1; // Energy for CF3 Forward/Reverse status +static const uint16_t ATM90E32_STATUS_S0_CF4REVST = 1 << 0; // Energy for CF4 Forward/Reverse status +static const uint16_t ATM90E32_STATUS_S1_FREQHIST = 1 << 15; // Frequency is greater than the high threshold +static const uint16_t ATM90E32_STATUS_S1_SAGPHASEAST = 1 << 14; // Voltage sag on phase A +static const uint16_t ATM90E32_STATUS_S1_SAGPHASEBST = 1 << 13; // Voltage sag on phase B +static const uint16_t ATM90E32_STATUS_S1_SAGPHASECST = 1 << 12; // Voltage sag on phase C +static const uint16_t ATM90E32_STATUS_S1_FREQLOST = 1 << 11; // Frequency is lesser than the low threshold +static const uint16_t ATM90E32_STATUS_S1_PHASELOSSAST = 1 << 10; // Phase loss in Phase A +static const uint16_t ATM90E32_STATUS_S1_PHASELOSSBST = 1 << 9; // Phase loss in Phase B +static const uint16_t ATM90E32_STATUS_S1_PHASELOSSCST = 1 << 8; // Phase loss in Phase C +static const uint16_t ATM90E32_STATUS_S1_QEREGTPST = 1 << 7; // ReActive Energy register of sum (T) Positive Status +static const uint16_t ATM90E32_STATUS_S1_QEREGAPST = 1 << 6; // ReActive Energy register of Channel A Positive Status +static const uint16_t ATM90E32_STATUS_S1_QEREGBPST = 1 << 5; // ReActive Energy register of Channel B Positive Status +static const uint16_t ATM90E32_STATUS_S1_QEREGCPST = 1 << 4; // ReActive Energy register of Channel C Positive Status +static const uint16_t ATM90E32_STATUS_S1_PEREGTPST = 1 << 3; // Active Energy register of sum (T) Positive Status +static const uint16_t ATM90E32_STATUS_S1_PEREGAPST = 1 << 2; // Active Energy register of Channel A Positive Status +static const uint16_t ATM90E32_STATUS_S1_PEREGBPST = 1 << 1; // Active Energy register of Channel B Positive Status +static const uint16_t ATM90E32_STATUS_S1_PEREGCPST = 1 << 0; // Active Energy register of Channel C Positive Status + +/* LOW POWER MODE REGISTERS - NOT USED */ +static const uint16_t ATM90E32_REGISTER_DETECTCTRL = 0x10; +static const uint16_t ATM90E32_REGISTER_DETECTTH1 = 0x11; +static const uint16_t ATM90E32_REGISTER_DETECTTH2 = 0x12; +static const uint16_t ATM90E32_REGISTER_DETECTTH3 = 0x13; +static const uint16_t ATM90E32_REGISTER_PMOFFSETA = 0x14; +static const uint16_t ATM90E32_REGISTER_PMOFFSETB = 0x15; +static const uint16_t ATM90E32_REGISTER_PMOFFSETC = 0x16; +static const uint16_t ATM90E32_REGISTER_PMPGA = 0x17; +static const uint16_t ATM90E32_REGISTER_PMIRMSA = 0x18; +static const uint16_t ATM90E32_REGISTER_PMIRMSB = 0x19; +static const uint16_t ATM90E32_REGISTER_PMIRMSC = 0x1A; +static const uint16_t ATM90E32_REGISTER_PMCONFIG = 0x10B; +static const uint16_t ATM90E32_REGISTER_PMAVGSAMPLES = 0x1C; +static const uint16_t ATM90E32_REGISTER_PMIRMSLSB = 0x1D; + +/* CONFIGURATION REGISTERS */ +static const uint16_t ATM90E32_REGISTER_PLCONSTH = 0x31; // High Word of PL_Constant +static const uint16_t ATM90E32_REGISTER_PLCONSTL = 0x32; // Low Word of PL_Constant +static const uint16_t ATM90E32_REGISTER_MMODE0 = 0x33; // Metering Mode Config +static const uint16_t ATM90E32_REGISTER_MMODE1 = 0x34; // PGA Gain Configuration for Current Channels +static const uint16_t ATM90E32_REGISTER_PSTARTTH = 0x35; // Startup Power Th (P) +static const uint16_t ATM90E32_REGISTER_QSTARTTH = 0x36; // Startup Power Th (Q) +static const uint16_t ATM90E32_REGISTER_SSTARTTH = 0x37; // Startup Power Th (S) +static const uint16_t ATM90E32_REGISTER_PPHASETH = 0x38; // Startup Power Accum Th (P) +static const uint16_t ATM90E32_REGISTER_QPHASETH = 0x39; // Startup Power Accum Th (Q) +static const uint16_t ATM90E32_REGISTER_SPHASETH = 0x3A; // Startup Power Accum Th (S) + +/* CALIBRATION REGISTERS */ +static const uint16_t ATM90E32_REGISTER_POFFSETA = 0x41; // A Line Power Offset (P) +static const uint16_t ATM90E32_REGISTER_QOFFSETA = 0x42; // A Line Power Offset (Q) +static const uint16_t ATM90E32_REGISTER_POFFSETB = 0x43; // B Line Power Offset (P) +static const uint16_t ATM90E32_REGISTER_QOFFSETB = 0x44; // B Line Power Offset (Q) +static const uint16_t ATM90E32_REGISTER_POFFSETC = 0x45; // C Line Power Offset (P) +static const uint16_t ATM90E32_REGISTER_QOFFSETC = 0x46; // C Line Power Offset (Q) +static const uint16_t ATM90E32_REGISTER_PQGAINA = 0x47; // A Line Calibration Gain +static const uint16_t ATM90E32_REGISTER_PHIA = 0x48; // A Line Calibration Angle +static const uint16_t ATM90E32_REGISTER_PQGAINB = 0x49; // B Line Calibration Gain +static const uint16_t ATM90E32_REGISTER_PHIB = 0x4A; // B Line Calibration Angle +static const uint16_t ATM90E32_REGISTER_PQGAINC = 0x4B; // C Line Calibration Gain +static const uint16_t ATM90E32_REGISTER_PHIC = 0x4C; // C Line Calibration Angle + +/* FUNDAMENTAL/HARMONIC ENERGY CALIBRATION REGISTERS */ +static const uint16_t ATM90E32_REGISTER_POFFSETAF = 0x51; // A Fund Power Offset (P) +static const uint16_t ATM90E32_REGISTER_POFFSETBF = 0x52; // B Fund Power Offset (P) +static const uint16_t ATM90E32_REGISTER_POFFSETCF = 0x53; // C Fund Power Offset (P) +static const uint16_t ATM90E32_REGISTER_PGAINAF = 0x54; // A Fund Power Gain (P) +static const uint16_t ATM90E32_REGISTER_PGAINBF = 0x55; // B Fund Power Gain (P) +static const uint16_t ATM90E32_REGISTER_PGAINCF = 0x56; // C Fund Power Gain (P) + +/* MEASUREMENT CALIBRATION REGISTERS */ +static const uint16_t ATM90E32_REGISTER_UGAINA = 0x61; // A Voltage RMS Gain +static const uint16_t ATM90E32_REGISTER_IGAINA = 0x62; // A Current RMS Gain +static const uint16_t ATM90E32_REGISTER_UOFFSETA = 0x63; // A Voltage Offset +static const uint16_t ATM90E32_REGISTER_IOFFSETA = 0x64; // A Current Offset +static const uint16_t ATM90E32_REGISTER_UGAINB = 0x65; // B Voltage RMS Gain +static const uint16_t ATM90E32_REGISTER_IGAINB = 0x66; // B Current RMS Gain +static const uint16_t ATM90E32_REGISTER_UOFFSETB = 0x67; // B Voltage Offset +static const uint16_t ATM90E32_REGISTER_IOFFSETB = 0x68; // B Current Offset +static const uint16_t ATM90E32_REGISTER_UGAINC = 0x69; // C Voltage RMS Gain +static const uint16_t ATM90E32_REGISTER_IGAINC = 0x6A; // C Current RMS Gain +static const uint16_t ATM90E32_REGISTER_UOFFSETC = 0x6B; // C Voltage Offset +static const uint16_t ATM90E32_REGISTER_IOFFSETC = 0x6C; // C Current Offset +static const uint16_t ATM90E32_REGISTER_IOFFSETN = 0x6E; // N Current Offset + +/* ENERGY REGISTERS */ +static const uint16_t ATM90E32_REGISTER_APENERGYT = 0x80; // Total Forward Active +static const uint16_t ATM90E32_REGISTER_APENERGYA = 0x81; // A Forward Active +static const uint16_t ATM90E32_REGISTER_APENERGYB = 0x82; // B Forward Active +static const uint16_t ATM90E32_REGISTER_APENERGYC = 0x83; // C Forward Active +static const uint16_t ATM90E32_REGISTER_ANENERGYT = 0x84; // Total Reverse Active +static const uint16_t ATM90E32_REGISTER_ANENERGYA = 0x85; // A Reverse Active +static const uint16_t ATM90E32_REGISTER_ANENERGYB = 0x86; // B Reverse Active +static const uint16_t ATM90E32_REGISTER_ANENERGYC = 0x87; // C Reverse Active +static const uint16_t ATM90E32_REGISTER_RPENERGYT = 0x88; // Total Forward Reactive +static const uint16_t ATM90E32_REGISTER_RPENERGYA = 0x89; // A Forward Reactive +static const uint16_t ATM90E32_REGISTER_RPENERGYB = 0x8A; // B Forward Reactive +static const uint16_t ATM90E32_REGISTER_RPENERGYC = 0x8B; // C Forward Reactive +static const uint16_t ATM90E32_REGISTER_RNENERGYT = 0x8C; // Total Reverse Reactive +static const uint16_t ATM90E32_REGISTER_RNENERGYA = 0x8D; // A Reverse Reactive +static const uint16_t ATM90E32_REGISTER_RNENERGYB = 0x8E; // B Reverse Reactive +static const uint16_t ATM90E32_REGISTER_RNENERGYC = 0x8F; // C Reverse Reactive + +static const uint16_t ATM90E32_REGISTER_SAENERGYT = 0x90; // Total Apparent Energy +static const uint16_t ATM90E32_REGISTER_SENERGYA = 0x91; // A Apparent Energy +static const uint16_t ATM90E32_REGISTER_SENERGYB = 0x92; // B Apparent Energy +static const uint16_t ATM90E32_REGISTER_SENERGYC = 0x93; // C Apparent Energy + +/* FUNDAMENTAL / HARMONIC ENERGY REGISTERS */ +static const uint16_t ATM90E32_REGISTER_APENERGYTF = 0xA0; // Total Forward Fund. Energy +static const uint16_t ATM90E32_REGISTER_APENERGYAF = 0xA1; // A Forward Fund. Energy +static const uint16_t ATM90E32_REGISTER_APENERGYBF = 0xA2; // B Forward Fund. Energy +static const uint16_t ATM90E32_REGISTER_APENERGYCF = 0xA3; // C Forward Fund. Energy +static const uint16_t ATM90E32_REGISTER_ANENERGYTF = 0xA4; // Total Reverse Fund Energy +static const uint16_t ATM90E32_REGISTER_ANENERGYAF = 0xA5; // A Reverse Fund. Energy +static const uint16_t ATM90E32_REGISTER_ANENERGYBF = 0xA6; // B Reverse Fund. Energy +static const uint16_t ATM90E32_REGISTER_ANENERGYCF = 0xA7; // C Reverse Fund. Energy +static const uint16_t ATM90E32_REGISTER_APENERGYTH = 0xA8; // Total Forward Harm. Energy +static const uint16_t ATM90E32_REGISTER_APENERGYAH = 0xA9; // A Forward Harm. Energy +static const uint16_t ATM90E32_REGISTER_APENERGYBH = 0xAA; // B Forward Harm. Energy +static const uint16_t ATM90E32_REGISTER_APENERGYCH = 0xAB; // C Forward Harm. Energy +static const uint16_t ATM90E32_REGISTER_ANENERGYTH = 0xAC; // Total Reverse Harm. Energy +static const uint16_t ATM90E32_REGISTER_ANENERGYAH = 0xAD; // A Reverse Harm. Energy +static const uint16_t ATM90E32_REGISTER_ANENERGYBH = 0xAE; // B Reverse Harm. Energy +static const uint16_t ATM90E32_REGISTER_ANENERGYCH = 0xAF; // C Reverse Harm. Energy + +/* POWER & P.F. REGISTERS */ +static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0; // Total Mean Power (P) +static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1; // A Mean Power (P) +static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2; // B Mean Power (P) +static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3; // C Mean Power (P) +static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4; // Total Mean Power (Q) +static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5; // A Mean Power (Q) +static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6; // B Mean Power (Q) +static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7; // C Mean Power (Q) +static const uint16_t ATM90E32_REGISTER_SMEANT = 0xB8; // Total Mean Power (S) +static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9; // A Mean Power (S) +static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA; // B Mean Power (S) +static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB; // C Mean Power (S) +static const uint16_t ATM90E32_REGISTER_PFMEANT = 0xBC; // Mean Power Factor +static const uint16_t ATM90E32_REGISTER_PFMEANA = 0xBD; // A Power Factor +static const uint16_t ATM90E32_REGISTER_PFMEANB = 0xBE; // B Power Factor +static const uint16_t ATM90E32_REGISTER_PFMEANC = 0xBF; // C Power Factor + +static const uint16_t ATM90E32_REGISTER_PMEANTLSB = 0xC0; // Lower Word (Tot. Act. Power) +static const uint16_t ATM90E32_REGISTER_PMEANALSB = 0xC1; // Lower Word (A Act. Power) +static const uint16_t ATM90E32_REGISTER_PMEANBLSB = 0xC2; // Lower Word (B Act. Power) +static const uint16_t ATM90E32_REGISTER_PMEANCLSB = 0xC3; // Lower Word (C Act. Power) +static const uint16_t ATM90E32_REGISTER_QMEANTLSB = 0xC4; // Lower Word (Tot. React. Power) +static const uint16_t ATM90E32_REGISTER_QMEANALSB = 0xC5; // Lower Word (A React. Power) +static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6; // Lower Word (B React. Power) +static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7; // Lower Word (C React. Power) +static const uint16_t ATM90E32_REGISTER_SAMEANTLSB = 0xC8; // Lower Word (Tot. App. Power) +static const uint16_t ATM90E32_REGISTER_SMEANALSB = 0xC9; // Lower Word (A App. Power) +static const uint16_t ATM90E32_REGISTER_SMEANBLSB = 0xCA; // Lower Word (B App. Power) +static const uint16_t ATM90E32_REGISTER_SMEANCLSB = 0xCB; // Lower Word (C App. Power) + +/* FUND/HARM POWER & V/I RMS REGISTERS */ +static const uint16_t ATM90E32_REGISTER_PMEANTF = 0xD0; // Total Active Fund. Power +static const uint16_t ATM90E32_REGISTER_PMEANAF = 0xD1; // A Active Fund. Power +static const uint16_t ATM90E32_REGISTER_PMEANBF = 0xD2; // B Active Fund. Power +static const uint16_t ATM90E32_REGISTER_PMEANCF = 0xD3; // C Active Fund. Power +static const uint16_t ATM90E32_REGISTER_PMEANTH = 0xD4; // Total Active Harm. Power +static const uint16_t ATM90E32_REGISTER_PMEANAH = 0xD5; // A Active Harm. Power +static const uint16_t ATM90E32_REGISTER_PMEANBH = 0xD6; // B Active Harm. Power +static const uint16_t ATM90E32_REGISTER_PMEANCH = 0xD7; // C Active Harm. Power +static const uint16_t ATM90E32_REGISTER_URMSA = 0xD9; // A RMS Voltage +static const uint16_t ATM90E32_REGISTER_URMSB = 0xDA; // B RMS Voltage +static const uint16_t ATM90E32_REGISTER_URMSC = 0xDB; // C RMS Voltage +static const uint16_t ATM90E32_REGISTER_IRMSA = 0xDD; // A RMS Current +static const uint16_t ATM90E32_REGISTER_IRMSB = 0xDE; // B RMS Current +static const uint16_t ATM90E32_REGISTER_IRMSC = 0xDF; // C RMS Current +static const uint16_t ATM90E32_REGISTER_IRMSN = 0xD8; // Calculated N RMS Current + +static const uint16_t ATM90E32_REGISTER_PMEANTFLSB = 0xE0; // Lower Word (Tot. Act. Fund. Power) +static const uint16_t ATM90E32_REGISTER_PMEANAFLSB = 0xE1; // Lower Word (A Act. Fund. Power) +static const uint16_t ATM90E32_REGISTER_PMEANBFLSB = 0xE2; // Lower Word (B Act. Fund. Power) +static const uint16_t ATM90E32_REGISTER_PMEANCFLSB = 0xE3; // Lower Word (C Act. Fund. Power) +static const uint16_t ATM90E32_REGISTER_PMEANTHLSB = 0xE4; // Lower Word (Tot. Act. Harm. Power) +static const uint16_t ATM90E32_REGISTER_PMEANAHLSB = 0xE5; // Lower Word (A Act. Harm. Power) +static const uint16_t ATM90E32_REGISTER_PMEANBHLSB = 0xE6; // Lower Word (B Act. Harm. Power) +static const uint16_t ATM90E32_REGISTER_PMEANCHLSB = 0xE7; // Lower Word (C Act. Harm. Power) +static const uint16_t ATM90E32_REGISTER_URMSALSB = 0xE9; // Lower Word (A RMS Voltage) +static const uint16_t ATM90E32_REGISTER_URMSBLSB = 0xEA; // Lower Word (B RMS Voltage) +static const uint16_t ATM90E32_REGISTER_URMSCLSB = 0xEB; // Lower Word (C RMS Voltage) +static const uint16_t ATM90E32_REGISTER_IRMSALSB = 0xED; // Lower Word (A RMS Current) +static const uint16_t ATM90E32_REGISTER_IRMSBLSB = 0xEE; // Lower Word (B RMS Current) +static const uint16_t ATM90E32_REGISTER_IRMSCLSB = 0xEF; // Lower Word (C RMS Current) + +/* THD, FREQUENCY, ANGLE & TEMPTEMP REGISTERS*/ +static const uint16_t ATM90E32_REGISTER_THDNUA = 0xF1; // A Voltage THD+N +static const uint16_t ATM90E32_REGISTER_THDNUB = 0xF2; // B Voltage THD+N +static const uint16_t ATM90E32_REGISTER_THDNUC = 0xF3; // C Voltage THD+N +static const uint16_t ATM90E32_REGISTER_THDNIA = 0xF5; // A Current THD+N +static const uint16_t ATM90E32_REGISTER_THDNIB = 0xF6; // B Current THD+N +static const uint16_t ATM90E32_REGISTER_THDNIC = 0xF7; // C Current THD+N +static const uint16_t ATM90E32_REGISTER_FREQ = 0xF8; // Frequency +static const uint16_t ATM90E32_REGISTER_PANGLEA = 0xF9; // A Mean Phase Angle +static const uint16_t ATM90E32_REGISTER_PANGLEB = 0xFA; // B Mean Phase Angle +static const uint16_t ATM90E32_REGISTER_PANGLEC = 0xFB; // C Mean Phase Angle +static const uint16_t ATM90E32_REGISTER_TEMP = 0xFC; // Measured Temperature +static const uint16_t ATM90E32_REGISTER_UANGLEA = 0xFD; // A Voltage Phase Angle +static const uint16_t ATM90E32_REGISTER_UANGLEB = 0xFE; // B Voltage Phase Angle +static const uint16_t ATM90E32_REGISTER_UANGLEC = 0xFF; // C Voltage Phase Angle + +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py new file mode 100644 index 0000000000..7b62740f8e --- /dev/null +++ b/esphome/components/atm90e32/sensor.py @@ -0,0 +1,72 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, spi +from esphome.const import \ + CONF_ID, CONF_VOLTAGE, CONF_CURRENT, CONF_POWER, CONF_FREQUENCY, \ + ICON_FLASH, UNIT_HZ, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT + +CONF_PHASE_A = 'phase_a' +CONF_PHASE_B = 'phase_b' +CONF_PHASE_C = 'phase_c' + +CONF_LINE_FREQUENCY = 'line_frequency' +CONF_GAIN_PGA = 'gain_pga' +CONF_GAIN_VOLTAGE = 'gain_voltage' +CONF_GAIN_CT = 'gain_ct' +LINE_FREQS = { + '50HZ': 50, + '60HZ': 60, +} +PGA_GAINS = { + '1X': 0x0, + '2X': 0x15, + '4X': 0x2A, +} + +atm90e32_ns = cg.esphome_ns.namespace('atm90e32') +ATM90E32Component = atm90e32_ns.class_('ATM90E32Component', cg.PollingComponent, spi.SPIDevice) + +ATM90E32_PHASE_SCHEMA = cv.Schema({ + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2), + cv.Optional(CONF_GAIN_VOLTAGE, default=41820): cv.uint16_t, + cv.Optional(CONF_GAIN_CT, default=25498): cv.uint16_t, +}) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(ATM90E32Component), + cv.Optional(CONF_PHASE_A): ATM90E32_PHASE_SCHEMA, + cv.Optional(CONF_PHASE_B): ATM90E32_PHASE_SCHEMA, + cv.Optional(CONF_PHASE_C): ATM90E32_PHASE_SCHEMA, + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HZ, ICON_FLASH, 1), + cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), + cv.Optional(CONF_GAIN_PGA, default='2X'): cv.enum(PGA_GAINS, upper=True), +}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + + for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]): + if phase not in config: + continue + conf = config[phase] + cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE])) + cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT])) + if CONF_VOLTAGE in conf: + sens = yield sensor.new_sensor(conf[CONF_VOLTAGE]) + cg.add(var.set_voltage_sensor(i, sens)) + if CONF_CURRENT in conf: + sens = yield sensor.new_sensor(conf[CONF_CURRENT]) + cg.add(var.set_current_sensor(i, sens)) + if CONF_POWER in conf: + sens = yield sensor.new_sensor(conf[CONF_POWER]) + cg.add(var.set_power_sensor(i, sens)) + if CONF_FREQUENCY in config: + sens = yield sensor.new_sensor(config[CONF_FREQUENCY]) + cg.add(var.set_freq_sensor(sens)) + cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) + cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 1bdabaec37..978abae52a 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -46,52 +46,51 @@ climate::ClimateTraits BangBangClimate::traits() { traits.set_supports_heat_mode(this->supports_heat_); traits.set_supports_two_point_target_temperature(true); traits.set_supports_away(this->supports_away_); + traits.set_supports_action(true); return traits; } void BangBangClimate::compute_state_() { if (this->mode != climate::CLIMATE_MODE_AUTO) { // in non-auto mode - this->switch_to_mode_(this->mode); + this->switch_to_action_(static_cast<climate::ClimateAction>(this->mode)); return; } - - // auto mode, compute target mode if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) { // if any control values are nan, go to OFF (idle) mode - this->switch_to_mode_(climate::CLIMATE_MODE_OFF); + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); return; } const bool too_cold = this->current_temperature < this->target_temperature_low; const bool too_hot = this->current_temperature > this->target_temperature_high; - climate::ClimateMode target_mode; + climate::ClimateAction target_action; if (too_cold) { // too cold -> enable heating if possible, else idle if (this->supports_heat_) - target_mode = climate::CLIMATE_MODE_HEAT; + target_action = climate::CLIMATE_ACTION_HEATING; else - target_mode = climate::CLIMATE_MODE_OFF; + target_action = climate::CLIMATE_ACTION_OFF; } else if (too_hot) { // too hot -> enable cooling if possible, else idle if (this->supports_cool_) - target_mode = climate::CLIMATE_MODE_COOL; + target_action = climate::CLIMATE_ACTION_COOLING; else - target_mode = climate::CLIMATE_MODE_OFF; + target_action = climate::CLIMATE_ACTION_OFF; } else { // neither too hot nor too cold -> in range if (this->supports_cool_ && this->supports_heat_) { // if supports both ends, go to idle mode - target_mode = climate::CLIMATE_MODE_OFF; + target_action = climate::CLIMATE_ACTION_OFF; } else { // else use current mode and don't change (hysteresis) - target_mode = this->internal_mode_; + target_action = this->action; } } - this->switch_to_mode_(target_mode); + this->switch_to_action_(target_action); } -void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { - if (mode == this->internal_mode_) +void BangBangClimate::switch_to_action_(climate::ClimateAction action) { + if (action == this->action) // already in target mode return; @@ -100,14 +99,14 @@ void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { this->prev_trigger_ = nullptr; } Trigger<> *trig; - switch (mode) { - case climate::CLIMATE_MODE_OFF: + switch (action) { + case climate::CLIMATE_ACTION_OFF: trig = this->idle_trigger_; break; - case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_ACTION_COOLING: trig = this->cool_trigger_; break; - case climate::CLIMATE_MODE_HEAT: + case climate::CLIMATE_ACTION_HEATING: trig = this->heat_trigger_; break; default: @@ -116,7 +115,7 @@ void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { if (trig != nullptr) { // trig should never be null, but still check so that we don't crash trig->trigger(); - this->internal_mode_ = mode; + this->action = action; this->prev_trigger_ = trig; this->publish_state(); } @@ -146,6 +145,14 @@ Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; } void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } +void BangBangClimate::dump_config() { + LOG_CLIMATE("", "Bang Bang Climate", this); + ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); + ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); +} BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig() = default; BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_temperature_low, diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 716655d20f..84bcd51f34 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -21,6 +21,7 @@ class BangBangClimate : public climate::Climate, public Component { public: BangBangClimate(); void setup() override; + void dump_config() override; void set_sensor(sensor::Sensor *sensor); Trigger<> *get_idle_trigger() const; @@ -43,7 +44,7 @@ class BangBangClimate : public climate::Climate, public Component { void compute_state_(); /// Switch the climate device to the given climate mode. - void switch_to_mode_(climate::ClimateMode mode); + void switch_to_action_(climate::ClimateAction action); /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -74,11 +75,6 @@ class BangBangClimate : public climate::Climate, public Component { * This is so that the previous trigger can be stopped before enabling a new one. */ Trigger<> *prev_trigger_{nullptr}; - /** The climate mode that is currently active - for a `.mode = AUTO` this will - * contain the actual mode the device - * - */ - climate::ClimateMode internal_mode_{climate::CLIMATE_MODE_OFF}; BangBangClimateTargetTempConfig normal_config_{}; bool supports_away_{false}; diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index 6ba04ce355..dbfe1a8286 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -24,4 +24,4 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) - cg.add(var.set_oscillation(oscillation_output)) + cg.add(var.set_oscillating(oscillation_output)) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 9f92207b19..c082e2e9af 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -23,6 +23,7 @@ IS_PLATFORM_COMPONENT = True binary_sensor_ns = cg.esphome_ns.namespace('binary_sensor') BinarySensor = binary_sensor_ns.class_('BinarySensor', cg.Nameable) +BinarySensorInitiallyOff = binary_sensor_ns.class_('BinarySensorInitiallyOff', BinarySensor) BinarySensorPtr = BinarySensor.operator('ptr') # Triggers @@ -41,6 +42,7 @@ BinarySensorCondition = binary_sensor_ns.class_('BinarySensorCondition', Conditi # Filters Filter = binary_sensor_ns.class_('Filter') +DelayedOnOffFilter = binary_sensor_ns.class_('DelayedOnOffFilter', Filter, cg.Component) DelayedOnFilter = binary_sensor_ns.class_('DelayedOnFilter', Filter, cg.Component) DelayedOffFilter = binary_sensor_ns.class_('DelayedOffFilter', Filter, cg.Component) InvertFilter = binary_sensor_ns.class_('InvertFilter', Filter) @@ -55,6 +57,14 @@ def invert_filter_to_code(config, filter_id): yield cg.new_Pvariable(filter_id) +@FILTER_REGISTRY.register('delayed_on_off', DelayedOnOffFilter, + cv.positive_time_period_milliseconds) +def delayed_on_off_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id, config) + yield cg.register_component(var, {}) + yield var + + @FILTER_REGISTRY.register('delayed_on', DelayedOnFilter, cv.positive_time_period_milliseconds) def delayed_on_filter_to_code(config, filter_id): diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 27c835d38c..1cde692dd4 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -30,7 +30,11 @@ void BinarySensor::publish_initial_state(bool state) { } } void BinarySensor::send_state_internal(bool state, bool is_initial) { - ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state ? "ON" : "OFF"); + if (is_initial) { + ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state)); + } else { + ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), ONOFF(state)); + } this->has_state_ = true; this->state = state; if (!is_initial) { diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 51c7a57ff6..f91c93c424 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -10,9 +10,9 @@ namespace binary_sensor { #define LOG_BINARY_SENSOR(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ if (!obj->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Device Class: '%s'", obj->get_device_class().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); \ } \ } @@ -67,7 +67,7 @@ class BinarySensor : public Nameable { void send_state_internal(bool state, bool is_initial); /// Return whether this binary sensor has outputted a state. - bool has_state() const; + virtual bool has_state() const; virtual bool is_status_binary_sensor() const; @@ -86,5 +86,10 @@ class BinarySensor : public Nameable { Deduplicator<bool> publish_dedup_; }; +class BinarySensorInitiallyOff : public BinarySensor { + public: + bool has_state() const override { return true; } +}; + } // namespace binary_sensor } // namespace esphome diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index b7ac2c4a79..f4612d62e9 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -23,6 +23,19 @@ void Filter::input(bool value, bool is_initial) { this->output(*b, is_initial); } } + +DelayedOnOffFilter::DelayedOnOffFilter(uint32_t delay) : delay_(delay) {} +optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) { + if (value) { + this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(true, is_initial); }); + } else { + this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); }); + } + return {}; +} + +float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } + DelayedOnFilter::DelayedOnFilter(uint32_t delay) : delay_(delay) {} optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) { if (value) { @@ -46,6 +59,7 @@ optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) { return true; } } + float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; } diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index d1e9a0d23a..0b54251cda 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -25,6 +25,18 @@ class Filter { Deduplicator<bool> dedup_; }; +class DelayedOnOffFilter : public Filter, public Component { + public: + explicit DelayedOnOffFilter(uint32_t delay); + + optional<bool> new_value(bool value, bool is_initial) override; + + float get_setup_priority() const override; + + protected: + uint32_t delay_; +}; + class DelayedOnFilter : public Filter, public Component { public: explicit DelayedOnFilter(uint32_t delay); diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py index 8b8cd8fc4e..27f4654ded 100644 --- a/esphome/components/binary_sensor_map/sensor.py +++ b/esphome/components/binary_sensor_map/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor, binary_sensor from esphome.const import CONF_ID, CONF_CHANNELS, CONF_VALUE, CONF_TYPE, UNIT_EMPTY, \ - ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR + ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR, CONF_GROUP DEPENDENCIES = ['binary_sensor'] @@ -11,7 +11,6 @@ binary_sensor_map_ns = cg.esphome_ns.namespace('binary_sensor_map') BinarySensorMap = binary_sensor_map_ns.class_('BinarySensorMap', cg.Component, sensor.Sensor) SensorMapType = binary_sensor_map_ns.enum('SensorMapType') -CONF_GROUP = 'group' SENSOR_MAP_TYPES = { CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP, } diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 262cc3eedf..4c3f3af1e0 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -9,7 +9,7 @@ namespace esphome { namespace ble_presence { -class BLEPresenceDevice : public binary_sensor::BinarySensor, +class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index f32a0d2861..b7c7f12f6f 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -178,7 +178,7 @@ void BME280Component::update() { return; } - float meas_time = 1; + float meas_time = 1.5; meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_); meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f; meas_time += 2.3f * oversampling_to_time(this->humidity_oversampling_) + 0.575f; diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py new file mode 100644 index 0000000000..52885ae449 --- /dev/null +++ b/esphome/components/captive_portal/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import web_server_base +from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID +from esphome.const import CONF_ID +from esphome.core import coroutine_with_priority + +AUTO_LOAD = ['web_server_base'] +DEPENDENCIES = ['wifi'] + +captive_portal_ns = cg.esphome_ns.namespace('captive_portal') +CaptivePortal = captive_portal_ns.class_('CaptivePortal', cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(CaptivePortal), + cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(web_server_base.WebServerBase), +}).extend(cv.COMPONENT_SCHEMA) + + +@coroutine_with_priority(64.0) +def to_code(config): + paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) + + var = cg.new_Pvariable(config[CONF_ID], paren) + yield cg.register_component(var, config) + cg.add_define('USE_CAPTIVE_PORTAL') diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp new file mode 100644 index 0000000000..83f85d354c --- /dev/null +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -0,0 +1,174 @@ +#include "captive_portal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/wifi/wifi_component.h" + +namespace esphome { +namespace captive_portal { + +static const char *TAG = "captive_portal"; + +void CaptivePortal::handle_index(AsyncWebServerRequest *request) { + AsyncResponseStream *stream = request->beginResponseStream("text/html"); + stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" " + "content=\"width=device-width,initial-scale=1,user-scalable=no\"/><title>")); + stream->print(App.get_name().c_str()); + stream->print(F("</title><link rel=\"stylesheet\" href=\"/stylesheet.css\">")); + stream->print(F("<script>function c(l){document.getElementById('ssid').value=l.innerText||l.textContent; " + "document.getElementById('psk').focus();}</script>")); + stream->print(F("</head>")); + stream->print(F("<body><div class=\"main\"><h1>WiFi Networks</h1>")); + + if (request->hasArg("save")) { + stream->print(F("<div class=\"info\">The ESP will now try to connect to the network...<br/>Please give it some " + "time to connect.<br/>Note: Copy the changed network to your YAML file - the next OTA update will " + "overwrite these settings.</div>")); + } + + for (auto &scan : wifi::global_wifi_component->get_scan_result()) { + if (scan.get_is_hidden()) + continue; + + stream->print(F("<div class=\"network\" onclick=\"c(this)\"><a href=\"#\" class=\"network-left\">")); + + if (scan.get_rssi() >= -50) { + stream->print(F("<img src=\"/wifi-strength-4.svg\">")); + } else if (scan.get_rssi() >= -65) { + stream->print(F("<img src=\"/wifi-strength-3.svg\">")); + } else if (scan.get_rssi() >= -85) { + stream->print(F("<img src=\"/wifi-strength-2.svg\">")); + } else { + stream->print(F("<img src=\"/wifi-strength-1.svg\">")); + } + + stream->print(F("<span class=\"network-ssid\">")); + stream->print(scan.get_ssid().c_str()); + stream->print(F("</span></a>")); + if (scan.get_with_auth()) { + stream->print(F("<img src=\"/lock.svg\">")); + } + stream->print(F("</div>")); + } + + stream->print(F("<h3>WiFi Settings</h3><form method=\"GET\" action=\"/wifisave\"><input id=\"ssid\" name=\"ssid\" " + "length=32 placeholder=\"SSID\"><br/><input id=\"psk\" name=\"psk\" length=64 type=\"password\" " + "placeholder=\"Password\"><br/><br/><button type=\"submit\">Save</button></form><br><hr><br>")); + stream->print(F("<h1>OTA Update</h1><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input " + "type=\"file\" name=\"update\"><button type=\"submit\">Update</button></form>")); + stream->print(F("</div></body></html>")); + request->send(stream); +} +void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { + std::string ssid = request->arg("ssid").c_str(); + std::string psk = request->arg("psk").c_str(); + ESP_LOGI(TAG, "Captive Portal Requested WiFi Settings Change:"); + ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); + ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); + this->override_sta_(ssid, psk); + request->redirect("/?save=true"); +} +void CaptivePortal::override_sta_(const std::string &ssid, const std::string &password) { + CaptivePortalSettings save{}; + strcpy(save.ssid, ssid.c_str()); + strcpy(save.password, password.c_str()); + this->pref_.save(&save); + + wifi::WiFiAP sta{}; + sta.set_ssid(ssid); + sta.set_password(password); + wifi::global_wifi_component->set_sta(sta); +} + +void CaptivePortal::setup() { + // Hash with compilation time + // This ensures the AP override is not applied for OTA + uint32_t hash = fnv1_hash(App.get_compilation_time()); + this->pref_ = global_preferences.make_preference<CaptivePortalSettings>(hash, true); + + CaptivePortalSettings save{}; + if (this->pref_.load(&save)) { + this->override_sta_(save.ssid, save.password); + } +} +void CaptivePortal::start() { + this->base_->init(); + if (!this->initialized_) { + this->base_->add_handler(this); + this->base_->add_ota_handler(); + } + + this->dns_server_ = new DNSServer(); + this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); + IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); + this->dns_server_->start(53, "*", ip); + + this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { + bool not_found = false; + if (!this->active_) { + not_found = true; + } else if (req->host() == wifi::global_wifi_component->wifi_soft_ap_ip().toString()) { + not_found = true; + } + + if (not_found) { + req->send(404, "text/html", "File not found"); + return; + } + + auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().toString(); + req->redirect(url); + }); + + this->initialized_ = true; + this->active_ = true; +} + +const char STYLESHEET_CSS[] PROGMEM = + R"(*{box-sizing:inherit}div,input{padding:5px;font-size:1em}input{width:95%}body{text-align:center;font-family:sans-serif}button{border:0;border-radius:.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;padding:0}.main{text-align:left;display:inline-block;min-width:260px}.network{display:flex;justify-content:space-between;align-items:center}.network-left{display:flex;align-items:center}.network-ssid{margin-bottom:-7px;margin-left:10px}.info{border:1px solid;margin:10px 0;padding:15px 10px;color:#4f8a10;background-color:#dff2bf})"; +const char LOCK_SVG[] PROGMEM = + R"(<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z"/></svg>)"; + +void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { + if (req->url() == "/") { + this->handle_index(req); + return; + } else if (req->url() == "/wifisave") { + this->handle_wifisave(req); + return; + } else if (req->url() == "/stylesheet.css") { + req->send_P(200, "text/css", STYLESHEET_CSS); + return; + } else if (req->url() == "/lock.svg") { + req->send_P(200, "image/svg+xml", LOCK_SVG); + return; + } + + AsyncResponseStream *stream = req->beginResponseStream("image/svg+xml"); + stream->print(F("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\"><path d=\"M12 3A18.9 18.9 0 0 " + "0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 ")); + if (req->url() == "/wifi-strength-4.svg") { + stream->print(F("3z")); + } else { + if (req->url() == "/wifi-strength-1.svg") { + stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.4")); + } else if (req->url() == "/wifi-strength-2.svg") { + stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.4")); + } else if (req->url() == "/wifi-strength-3.svg") { + stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2.")); + } + stream->print(F("4A16.94 16.94 0 0 1 12 5z")); + } + stream->print(F("\"/></svg>")); + req->send(stream); +} +CaptivePortal::CaptivePortal(web_server_base::WebServerBase *base) : base_(base) { global_captive_portal = this; } +float CaptivePortal::get_setup_priority() const { + // Before WiFi + return setup_priority::WIFI + 1.0f; +} +void CaptivePortal::dump_config() { ESP_LOGCONFIG(TAG, "Captive Portal:"); } + +CaptivePortal *global_captive_portal = nullptr; + +} // namespace captive_portal +} // namespace esphome diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h new file mode 100644 index 0000000000..3af47546cf --- /dev/null +++ b/esphome/components/captive_portal/captive_portal.h @@ -0,0 +1,82 @@ +#pragma once + +#include <DNSServer.h> +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" +#include "esphome/components/web_server_base/web_server_base.h" + +namespace esphome { + +namespace captive_portal { + +struct CaptivePortalSettings { + char ssid[33]; + char password[65]; +} PACKED; // NOLINT + +class CaptivePortal : public AsyncWebHandler, public Component { + public: + CaptivePortal(web_server_base::WebServerBase *base); + void setup() override; + void dump_config() override; + void loop() override { + if (this->dns_server_ != nullptr) + this->dns_server_->processNextRequest(); + } + float get_setup_priority() const override; + void start(); + bool is_active() const { return this->active_; } + void end() { + this->active_ = false; + this->base_->deinit(); + this->dns_server_->stop(); + delete this->dns_server_; + } + + bool canHandle(AsyncWebServerRequest *request) override { + if (!this->active_) + return false; + + if (request->method() == HTTP_GET) { + if (request->url() == "/") + return true; + if (request->url() == "/stylesheet.css") + return true; + if (request->url() == "/wifi-strength-1.svg") + return true; + if (request->url() == "/wifi-strength-2.svg") + return true; + if (request->url() == "/wifi-strength-3.svg") + return true; + if (request->url() == "/wifi-strength-4.svg") + return true; + if (request->url() == "/lock.svg") + return true; + if (request->url() == "/wifisave") + return true; + } + + return false; + } + + void handle_index(AsyncWebServerRequest *request); + + void handle_wifisave(AsyncWebServerRequest *request); + + void handleRequest(AsyncWebServerRequest *req) override; + + protected: + void override_sta_(const std::string &ssid, const std::string &password); + + web_server_base::WebServerBase *base_; + bool initialized_{false}; + bool active_{false}; + ESPPreferenceObject pref_; + DNSServer *dns_server_{nullptr}; +}; + +extern CaptivePortal *global_captive_portal; + +} // namespace captive_portal +} // namespace esphome diff --git a/esphome/components/captive_portal/index.html b/esphome/components/captive_portal/index.html new file mode 100644 index 0000000000..627bf81215 --- /dev/null +++ b/esphome/components/captive_portal/index.html @@ -0,0 +1,55 @@ +<!-- HTTP_HEAD --> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/> + <title>{{ App.get_name() }}</title> + <link rel="stylesheet" href="./stylesheet.css"> + <script> + function c(l) { + document.getElementById('ssid').value = l.innerText || l.textContent; + document.getElementById('psk').focus(); + } + </script> +</head> +<body> +<div class="main"> + <h1>WiFi Networks</h1> + <div class="info"> + The ESP will now try to connect to the network...<br/> + Please give it some time to connect.<br/> + Note: Copy the changed network to your YAML file - the next OTA update will overwrite these settings. + </div> + <div class="network" onclick="c(this)"> + <a href="#" class="network-left"> + <img src="./wifi-strength-4.svg"> + <span class="network-ssid">AP1</span> + </a> + <img src="./lock.svg"> + </div> + <div class="network" onclick="c(this)"> + <a href="#" class="network-left"> + <img src="./wifi-strength-2.svg"> + <span class="network-ssid">AP2</span> + </a> + </div> + + <h3>WiFi Settings</h3> + <form method="GET" action="/wifisave"> + <input id="ssid" name="ssid" length=32 placeholder="SSID"><br/> + <input id="psk" name="psk" length=64 type="password" placeholder="Password"><br/> + <br/> + <button type="submit">Save</button> + </form> + <br><hr> + <br> + + <h1>OTA Update</h1> + <form method="POST" action="/update" enctype="multipart/form-data"> + <input type="file" name="update"> + <button type="submit">Update</button> + </form> +</div> +</body> +</html> diff --git a/esphome/components/captive_portal/lock.svg b/esphome/components/captive_portal/lock.svg new file mode 100644 index 0000000000..743a1cc55a --- /dev/null +++ b/esphome/components/captive_portal/lock.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z"/></svg> \ No newline at end of file diff --git a/esphome/components/captive_portal/stylesheet.css b/esphome/components/captive_portal/stylesheet.css new file mode 100644 index 0000000000..73f82f05f1 --- /dev/null +++ b/esphome/components/captive_portal/stylesheet.css @@ -0,0 +1,58 @@ +* { + box-sizing: inherit; +} + +div, input { + padding: 5px; + font-size: 1em; +} + +input { + width: 95%; +} + +body { + text-align: center; + font-family: sans-serif; +} + +button { + border: 0; + border-radius: 0.3rem; + background-color: #1fa3ec; + color: #fff; + line-height: 2.4rem; + font-size: 1.2rem; + width: 100%; + padding: 0; +} + +.main { + text-align: left; + display: inline-block; + min-width: 260px; +} + +.network { + display: flex; + justify-content: space-between; + align-items: center; +} + +.network-left { + display: flex; + align-items: center; +} + +.network-ssid { + margin-bottom: -7px; + margin-left: 10px; +} + +.info { + border: 1px solid; + margin: 10px 0px; + padding: 15px 10px; + color: #4f8a10; + background-color: #dff2bf; +} diff --git a/esphome/components/captive_portal/wifi-strength-1.svg b/esphome/components/captive_portal/wifi-strength-1.svg new file mode 100644 index 0000000000..189a38193c --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-1.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.44A16.94 16.94 0 0 1 12 5z"/></svg> \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-2.svg b/esphome/components/captive_portal/wifi-strength-2.svg new file mode 100644 index 0000000000..9b4b2d2396 --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-2.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.44A16.94 16.94 0 0 1 12 5z"/></svg> \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-3.svg b/esphome/components/captive_portal/wifi-strength-3.svg new file mode 100644 index 0000000000..44b7532bb7 --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-3.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2.4A16.94 16.94 0 0 1 12 5z"/></svg> \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-4.svg b/esphome/components/captive_portal/wifi-strength-4.svg new file mode 100644 index 0000000000..a22b0b8281 --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-4.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3z"/></svg> \ No newline at end of file diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 2b40f932f9..7c7da6bb0c 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -173,6 +173,9 @@ void Climate::publish_state() { auto traits = this->get_traits(); ESP_LOGD(TAG, " Mode: %s", climate_mode_to_string(this->mode)); + if (traits.get_supports_action()) { + ESP_LOGD(TAG, " Action: %s", climate_action_to_string(this->action)); + } if (traits.get_supports_current_temperature()) { ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index c58eed1a7c..4dd872bbed 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -9,6 +9,11 @@ namespace esphome { namespace climate { +#define LOG_CLIMATE(prefix, type, obj) \ + if (obj != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ + } + class Climate; /** This class is used to encode all control actions on a climate device. @@ -121,6 +126,8 @@ class Climate : public Nameable { /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; + /// The active state of the climate device. + ClimateAction action{CLIMATE_ACTION_OFF}; /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 07b97f4f33..34aa564fb0 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -17,6 +17,18 @@ const char *climate_mode_to_string(ClimateMode mode) { return "UNKNOWN"; } } +const char *climate_action_to_string(ClimateAction action) { + switch (action) { + case CLIMATE_ACTION_OFF: + return "OFF"; + case CLIMATE_ACTION_COOLING: + return "COOLING"; + case CLIMATE_ACTION_HEATING: + return "HEATING"; + default: + return "UNKNOWN"; + } +} } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 28608b7cd8..e5786286d8 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -17,8 +17,19 @@ enum ClimateMode : uint8_t { CLIMATE_MODE_HEAT = 3, }; +/// Enum for the current action of the climate device. Values match those of ClimateMode. +enum ClimateAction : uint8_t { + /// The climate device is off (inactive or no power) + CLIMATE_ACTION_OFF = 0, + /// The climate device is actively cooling (usually in cool or auto mode) + CLIMATE_ACTION_COOLING = 2, + /// The climate device is actively heating (usually in heat or auto mode) + CLIMATE_ACTION_HEATING = 3, +}; + /// Convert the given ClimateMode to a human-readable string. const char *climate_mode_to_string(ClimateMode mode); +const char *climate_action_to_string(ClimateAction action); } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 712186aa80..a1db2bc696 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -30,6 +30,7 @@ void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_a void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; } void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; } void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; } +void ClimateTraits::set_supports_action(bool supports_action) { supports_action_ = supports_action; } float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; } void ClimateTraits::set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; @@ -52,6 +53,7 @@ int8_t ClimateTraits::get_temperature_accuracy_decimals() const { } void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } bool ClimateTraits::get_supports_away() const { return supports_away_; } +bool ClimateTraits::get_supports_action() const { return supports_action_; } } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 34e03455b1..2d6f44eea6 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -23,6 +23,8 @@ namespace climate { * - heat mode (increases current temperature) * - supports away - away mode means that the climate device supports two different * target temperature settings: one target temp setting for "away" mode and one for non-away mode. + * - supports action - if the climate device supports reporting the active + * current action of the device with the action property. * * This class also contains static data for the climate device display: * - visual min/max temperature - tells the frontend what range of temperatures the climate device @@ -41,6 +43,8 @@ class ClimateTraits { void set_supports_heat_mode(bool supports_heat_mode); void set_supports_away(bool supports_away); bool get_supports_away() const; + void set_supports_action(bool supports_action); + bool get_supports_action() const; bool supports_mode(ClimateMode mode) const; float get_visual_min_temperature() const; @@ -58,6 +62,7 @@ class ClimateTraits { bool supports_cool_mode_{false}; bool supports_heat_mode_{false}; bool supports_away_{false}; + bool supports_action_{false}; float visual_min_temperature_{10}; float visual_max_temperature_{30}; diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py new file mode 100644 index 0000000000..1163705faa --- /dev/null +++ b/esphome/components/climate_ir/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, remote_transmitter, remote_receiver, sensor, remote_base +from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID +from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR +from esphome.core import coroutine + +AUTO_LOAD = ['sensor', 'remote_base'] + +climate_ir_ns = cg.esphome_ns.namespace('climate_ir') +ClimateIR = climate_ir_ns.class_('ClimateIR', climate.Climate, cg.Component, + remote_base.RemoteReceiverListener) + +CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), + cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), +}).extend(cv.COMPONENT_SCHEMA) + +CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend({ + cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent), +}) + + +@coroutine +def register_climate_ir(var, config): + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) + cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) + if CONF_SENSOR in config: + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + if CONF_RECEIVER_ID in config: + receiver = yield cg.get_variable(config[CONF_RECEIVER_ID]) + cg.add(receiver.register_listener(var)) + + transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp new file mode 100644 index 0000000000..4b9a1c0baa --- /dev/null +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -0,0 +1,67 @@ +#include "climate_ir.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace climate_ir { + +static const char *TAG = "climate_ir"; + +climate::ClimateTraits ClimateIR::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->sensor_ != nullptr); + traits.set_supports_auto_mode(true); + traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_two_point_target_temperature(false); + traits.set_supports_away(false); + traits.set_visual_min_temperature(this->minimum_temperature_); + traits.set_visual_max_temperature(this->maximum_temperature_); + traits.set_visual_temperature_step(this->temperature_step_); + return traits; +} + +void ClimateIR::setup() { + if (this->sensor_) { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + // current temperature changed, publish state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + } else + this->current_temperature = NAN; + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } else { + // restore from defaults + this->mode = climate::CLIMATE_MODE_OFF; + // initialize target temperature to some value so that it's not NAN + this->target_temperature = + roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_)); + } + // Never send nan to HA + if (isnan(this->target_temperature)) + this->target_temperature = 24; +} + +void ClimateIR::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + + this->transmit_state(); + this->publish_state(); +} +void ClimateIR::dump_config() { + LOG_CLIMATE("", "IR Climate", this); + ESP_LOGCONFIG(TAG, " Min. Temperature: %.1f°C", this->minimum_temperature_); + ESP_LOGCONFIG(TAG, " Max. Temperature: %.1f°C", this->maximum_temperature_); + ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); +} + +} // namespace climate_ir +} // namespace esphome diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h new file mode 100644 index 0000000000..b4c036f3d6 --- /dev/null +++ b/esphome/components/climate_ir/climate_ir.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/components/climate/climate.h" +#include "esphome/components/remote_base/remote_base.h" +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace climate_ir { + +/* A base for climate which works by sending (and receiving) IR codes + + To send IR codes implement + void ClimateIR::transmit_state_() + + Likewise to decode a IR into the AC state, implement + bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true +*/ +class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { + public: + ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f) { + this->minimum_temperature_ = minimum_temperature; + this->maximum_temperature_ = maximum_temperature; + this->temperature_step_ = temperature_step; + } + + void setup() override; + void dump_config() override; + void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { + this->transmitter_ = transmitter; + } + void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } + void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } + void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + + protected: + float minimum_temperature_, maximum_temperature_, temperature_step_; + + /// 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; + + /// Transmit via IR the state of this climate controller. + virtual void transmit_state() = 0; + + bool supports_cool_{true}; + bool supports_heat_{true}; + + remote_transmitter::RemoteTransmitterComponent *transmitter_; + sensor::Sensor *sensor_{nullptr}; +}; + +} // namespace climate_ir +} // namespace esphome diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 750a97d087..81412bb586 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -1,36 +1,18 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, sensor -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.components import climate_ir +from esphome.const import CONF_ID -AUTO_LOAD = ['sensor'] +AUTO_LOAD = ['climate_ir'] coolix_ns = cg.esphome_ns.namespace('coolix') -CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component) +CoolixClimate = coolix_ns.class_('CoolixClimate', climate_ir.ClimateIR) -CONF_TRANSMITTER_ID = 'transmitter_id' -CONF_SUPPORTS_HEAT = 'supports_heat' -CONF_SUPPORTS_COOL = 'supports_cool' - -CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(CoolixClimate), - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), - cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, - cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, - cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), -}).extend(cv.COMPONENT_SCHEMA)) +}) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield climate.register_climate(var, config) - - cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) - cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) - if CONF_SENSOR in config: - sens = yield cg.get_variable(config[CONF_SENSOR]) - cg.add(var.set_sensor(sens)) - - transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) - cg.add(var.set_transmitter(transmitter)) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index ffc67adeb3..c08571c2e9 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -7,19 +7,26 @@ namespace coolix { static const char *TAG = "coolix.climate"; const uint32_t COOLIX_OFF = 0xB27BE0; +const uint32_t COOLIX_SWING = 0xB26BE0; +const uint32_t COOLIX_LED = 0xB5F5A5; +const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6; + // On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. const uint32_t COOLIX_DEFAULT_STATE = 0xB2BFC8; const uint32_t COOLIX_DEFAULT_STATE_AUTO_24_FAN = 0xB21F48; -const uint8_t COOLIX_COOL = 0b00; -const uint8_t COOLIX_DRY = 0b01; -const uint8_t COOLIX_AUTO = 0b10; -const uint8_t COOLIX_HEAT = 0b11; -const uint8_t COOLIX_FAN = 4; // Synthetic. -const uint32_t COOLIX_MODE_MASK = 0b000000000000000000001100; // 0xC +const uint8_t COOLIX_COOL = 0b0000; +const uint8_t COOLIX_DRY_FAN = 0b0100; +const uint8_t COOLIX_AUTO = 0b1000; +const uint8_t COOLIX_HEAT = 0b1100; +const uint32_t COOLIX_MODE_MASK = 0b1100; +const uint32_t COOLIX_FAN_MASK = 0xF000; +const uint32_t COOLIX_FAN_DRY = 0x1000; +const uint32_t COOLIX_FAN_AUTO = 0xB000; +const uint32_t COOLIX_FAN_MIN = 0x9000; +const uint32_t COOLIX_FAN_MED = 0x5000; +const uint32_t COOLIX_FAN_MAX = 0x3000; // Temperature -const uint8_t COOLIX_TEMP_MIN = 17; // Celsius -const uint8_t COOLIX_TEMP_MAX = 30; // Celsius const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; const uint8_t COOLIX_FAN_TEMP_CODE = 0b1110; // Part of Fan Mode. const uint32_t COOLIX_TEMP_MASK = 0b11110000; @@ -41,80 +48,25 @@ const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { }; // Constants -// Pulse parms are *50-100 for the Mark and *50+100 for the space -// First MARK is the one after the long gap -// pulse parameters in usec -const uint16_t COOLIX_TICK = 560; // Approximately 21 cycles at 38kHz -const uint16_t COOLIX_BIT_MARK_TICKS = 1; -const uint16_t COOLIX_BIT_MARK = COOLIX_BIT_MARK_TICKS * COOLIX_TICK; -const uint16_t COOLIX_ONE_SPACE_TICKS = 3; -const uint16_t COOLIX_ONE_SPACE = COOLIX_ONE_SPACE_TICKS * COOLIX_TICK; -const uint16_t COOLIX_ZERO_SPACE_TICKS = 1; -const uint16_t COOLIX_ZERO_SPACE = COOLIX_ZERO_SPACE_TICKS * COOLIX_TICK; -const uint16_t COOLIX_HEADER_MARK_TICKS = 8; -const uint16_t COOLIX_HEADER_MARK = COOLIX_HEADER_MARK_TICKS * COOLIX_TICK; -const uint16_t COOLIX_HEADER_SPACE_TICKS = 8; -const uint16_t COOLIX_HEADER_SPACE = COOLIX_HEADER_SPACE_TICKS * COOLIX_TICK; -const uint16_t COOLIX_MIN_GAP_TICKS = COOLIX_HEADER_MARK_TICKS + COOLIX_ZERO_SPACE_TICKS; -const uint16_t COOLIX_MIN_GAP = COOLIX_MIN_GAP_TICKS * COOLIX_TICK; +static const uint32_t BIT_MARK_US = 660; +static const uint32_t HEADER_MARK_US = 560 * 8; +static const uint32_t HEADER_SPACE_US = 560 * 8; +static const uint32_t BIT_ONE_SPACE_US = 1500; +static const uint32_t BIT_ZERO_SPACE_US = 450; +static const uint32_t FOOTER_MARK_US = BIT_MARK_US; +static const uint32_t FOOTER_SPACE_US = HEADER_SPACE_US; const uint16_t COOLIX_BITS = 24; -climate::ClimateTraits CoolixClimate::traits() { - auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_auto_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_two_point_target_temperature(false); - traits.set_supports_away(false); - traits.set_visual_min_temperature(17); - traits.set_visual_max_temperature(30); - traits.set_visual_temperature_step(1); - return traits; -} - -void CoolixClimate::setup() { - if (this->sensor_) { - this->sensor_->add_on_state_callback([this](float state) { - this->current_temperature = state; - // current temperature changed, publish state - this->publish_state(); - }); - this->current_temperature = this->sensor_->state; - } else - this->current_temperature = NAN; - // restore set points - auto restore = this->restore_state_(); - if (restore.has_value()) { - restore->apply(this); - } else { - // restore from defaults - this->mode = climate::CLIMATE_MODE_AUTO; - // initialize target temperature to some value so that it's not NAN - this->target_temperature = roundf(this->current_temperature); - } -} - -void CoolixClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - - this->transmit_state_(); - this->publish_state(); -} - -void CoolixClimate::transmit_state_() { +void CoolixClimate::transmit_state() { uint32_t remote_state; switch (this->mode) { case climate::CLIMATE_MODE_COOL: - remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | (COOLIX_COOL << 2); + remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_COOL; break; case climate::CLIMATE_MODE_HEAT: - remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | (COOLIX_HEAT << 2); + remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_HEAT; break; case climate::CLIMATE_MODE_AUTO: remote_state = COOLIX_DEFAULT_STATE_AUTO_24_FAN; @@ -127,10 +79,10 @@ void CoolixClimate::transmit_state_() { if (this->mode != climate::CLIMATE_MODE_OFF) { auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); remote_state &= ~COOLIX_TEMP_MASK; // Clear the old temp. - remote_state |= (COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4); + remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4; } - ESP_LOGV(TAG, "Sending coolix code: %u", remote_state); + ESP_LOGV(TAG, "Sending coolix code: 0x%02X", remote_state); auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); @@ -139,32 +91,113 @@ void CoolixClimate::transmit_state_() { uint16_t repeat = 1; for (uint16_t r = 0; r <= repeat; r++) { // Header - data->mark(COOLIX_HEADER_MARK); - data->space(COOLIX_HEADER_SPACE); + data->mark(HEADER_MARK_US); + data->space(HEADER_SPACE_US); // Data - // Break data into byte segments, starting at the Most Significant + // Break data into bytes, starting at the Most Significant // Byte. Each byte then being sent normal, then followed inverted. for (uint16_t i = 8; i <= COOLIX_BITS; i += 8) { // Grab a bytes worth of data. - uint8_t segment = (remote_state >> (COOLIX_BITS - i)) & 0xFF; + uint8_t byte = (remote_state >> (COOLIX_BITS - i)) & 0xFF; // Normal for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - data->mark(COOLIX_BIT_MARK); - data->space((segment & mask) ? COOLIX_ONE_SPACE : COOLIX_ZERO_SPACE); + data->mark(BIT_MARK_US); + data->space((byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); } // Inverted for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - data->mark(COOLIX_BIT_MARK); - data->space(!(segment & mask) ? COOLIX_ONE_SPACE : COOLIX_ZERO_SPACE); + data->mark(BIT_MARK_US); + data->space(!(byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); } } // Footer - data->mark(COOLIX_BIT_MARK); - data->space(COOLIX_MIN_GAP); // Pause before repeating + data->mark(BIT_MARK_US); + data->space(FOOTER_SPACE_US); // Pause before repeating } transmit.perform(); } +bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { + // Decoded remote state y 3 bytes long code. + uint32_t remote_state = 0; + // The protocol sends the data twice, read here + uint32_t loop_read; + for (uint16_t loop = 1; loop <= 2; loop++) { + if (!data.expect_item(HEADER_MARK_US, HEADER_SPACE_US)) + return false; + loop_read = 0; + for (uint8_t a_byte = 0; a_byte < 3; a_byte++) { + uint8_t byte = 0; + for (int8_t a_bit = 7; a_bit >= 0; a_bit--) { + if (data.expect_item(BIT_MARK_US, BIT_ONE_SPACE_US)) + byte |= 1 << a_bit; + else if (!data.expect_item(BIT_MARK_US, BIT_ZERO_SPACE_US)) + return false; + } + // Need to see this segment inverted + for (int8_t a_bit = 7; a_bit >= 0; a_bit--) { + bool bit = byte & (1 << a_bit); + if (!data.expect_item(BIT_MARK_US, bit ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US)) + return false; + } + // Receiving MSB first: reorder bytes + loop_read |= byte << ((2 - a_byte) * 8); + } + // Footer Mark + if (!data.expect_mark(BIT_MARK_US)) + return false; + if (loop == 1) { + // Back up state on first loop + remote_state = loop_read; + if (!data.expect_space(FOOTER_SPACE_US)) + return false; + } + } + + ESP_LOGV(TAG, "Decoded 0x%02X", remote_state); + if (remote_state != loop_read || (remote_state & 0xFF0000) != 0xB20000) + return false; + + if (remote_state == COOLIX_OFF) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT) + this->mode = climate::CLIMATE_MODE_HEAT; + else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO) + this->mode = climate::CLIMATE_MODE_AUTO; + else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) { + // climate::CLIMATE_MODE_DRY; + if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_DRY) + ESP_LOGV(TAG, "Not supported DRY mode. Reporting AUTO"); + else + ESP_LOGV(TAG, "Not supported FAN Auto mode. Reporting AUTO"); + this->mode = climate::CLIMATE_MODE_AUTO; + } else + this->mode = climate::CLIMATE_MODE_COOL; + + // Fan Speed + // When climate::CLIMATE_MODE_DRY is implemented replace following line with this: + // if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_DRY) + if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO) + ESP_LOGV(TAG, "Not supported FAN speed AUTO"); + else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN) + ESP_LOGV(TAG, "Not supported FAN speed MIN"); + else if ((remote_state & COOLIX_FAN_MED) == COOLIX_FAN_MED) + ESP_LOGV(TAG, "Not supported FAN speed MED"); + else if ((remote_state & COOLIX_FAN_MAX) == COOLIX_FAN_MAX) + ESP_LOGV(TAG, "Not supported FAN speed MAX"); + + // Temperature + uint8_t temperature_code = (remote_state & COOLIX_TEMP_MASK) >> 4; + for (uint8_t i = 0; i < COOLIX_TEMP_RANGE; i++) + if (COOLIX_TEMP_MAP[i] == temperature_code) + this->target_temperature = i + COOLIX_TEMP_MIN; + } + this->publish_state(); + + return true; +} + } // namespace coolix } // namespace esphome diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index 0d52018d2a..ed03a2fd1e 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -1,39 +1,23 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/remote_base/remote_base.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" -#include "esphome/components/sensor/sensor.h" +#include "esphome/components/climate_ir/climate_ir.h" namespace esphome { namespace coolix { -class CoolixClimate : public climate::Climate, public Component { +// Temperature +const uint8_t COOLIX_TEMP_MIN = 17; // Celsius +const uint8_t COOLIX_TEMP_MAX = 30; // Celsius + +class CoolixClimate : public climate_ir::ClimateIR { public: - void setup() override; - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } - void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } - void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } - void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + CoolixClimate() : climate_ir::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX) {} 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; - /// Transmit via IR the state of this climate controller. - void transmit_state_(); - - bool supports_cool_{true}; - bool supports_heat_{true}; - - remote_transmitter::RemoteTransmitterComponent *transmitter_; - sensor::Sensor *sensor_{nullptr}; + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; }; } // namespace coolix diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 12011e1b4c..839cf9207e 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -13,13 +13,13 @@ const extern float COVER_CLOSED; #define LOG_COVER(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ auto traits_ = obj->get_traits(); \ if (traits_.get_is_assumed_state()) { \ - ESP_LOGCONFIG(TAG, prefix " Assumed State: YES"); \ + ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ if (!obj->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Device Class: '%s'", obj->get_device_class().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); \ } \ } diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 358453a63a..6c014138fd 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -172,6 +172,7 @@ void CSE7766Component::dump_config() { LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_); + this->check_uart_settings(4800); } } // namespace cse7766 diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index 1a7bac2844..674cc0ae98 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -8,6 +8,18 @@ namespace ct_clamp { static const char *TAG = "ct_clamp"; +void CTClampSensor::setup() { + this->is_calibrating_offset_ = true; + this->high_freq_.start(); + this->set_timeout("calibrate_offset", this->sample_duration_, [this]() { + this->high_freq_.stop(); + this->is_calibrating_offset_ = false; + if (this->num_samples_ != 0) { + this->offset_ = this->sample_sum_ / this->num_samples_; + } + }); +} + void CTClampSensor::dump_config() { LOG_SENSOR("", "CT Clamp Sensor", this); ESP_LOGCONFIG(TAG, " Sample Duration: %.2fs", this->sample_duration_ / 1e3f); @@ -15,6 +27,9 @@ void CTClampSensor::dump_config() { } void CTClampSensor::update() { + if (this->is_calibrating_offset_) + return; + // Update only starts the sampling phase, in loop() the actual sampling is happening. // Request a high loop() execution interval during sampling phase. @@ -44,12 +59,18 @@ void CTClampSensor::update() { } void CTClampSensor::loop() { - if (!this->is_sampling_) + if (!this->is_sampling_ && !this->is_calibrating_offset_) return; // Perform a single sample float value = this->source_->sample(); + if (this->is_calibrating_offset_) { + this->sample_sum_ += value; + this->num_samples_++; + return; + } + // Adjust DC offset via low pass filter (exponential moving average) const float alpha = 0.001f; this->offset_ = this->offset_ * (1 - alpha) + value * alpha; diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.h b/esphome/components/ct_clamp/ct_clamp_sensor.h index d816ac781a..c709f6718b 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.h +++ b/esphome/components/ct_clamp/ct_clamp_sensor.h @@ -10,10 +10,14 @@ namespace ct_clamp { class CTClampSensor : public sensor::Sensor, public PollingComponent { public: + void setup() override; void update() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } + float get_setup_priority() const override { + // After the base sensor has been initialized + return setup_priority::DATA - 1.0f; + } void set_sample_duration(uint32_t sample_duration) { sample_duration_ = sample_duration; } void set_source(voltage_sampler::VoltageSampler *source) { source_ = source; } @@ -40,6 +44,8 @@ class CTClampSensor : public sensor::Sensor, public PollingComponent { float sample_sum_ = 0.0f; uint32_t num_samples_ = 0; bool is_sampling_ = false; + /// Calibrate offset value once at boot + bool is_calibrating_offset_ = false; }; } // namespace ct_clamp diff --git a/esphome/components/custom/output/__init__.py b/esphome/components/custom/output/__init__.py index 6042863872..efe6f19dab 100644 --- a/esphome/components/custom/output/__init__.py +++ b/esphome/components/custom/output/__init__.py @@ -1,13 +1,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import output -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_OUTPUTS, CONF_TYPE +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_OUTPUTS, CONF_TYPE, CONF_BINARY from .. import custom_ns CustomBinaryOutputConstructor = custom_ns.class_('CustomBinaryOutputConstructor') CustomFloatOutputConstructor = custom_ns.class_('CustomFloatOutputConstructor') -CONF_BINARY = 'binary' CONF_FLOAT = 'float' CONFIG_SCHEMA = cv.typed_schema({ diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 1d3e693ff9..6eeddb1b56 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -132,10 +132,14 @@ void DallasComponent::update() { enable_interrupts(); if (!res) { + ESP_LOGW(TAG, "'%s' - Reseting bus for read failed!", sensor->get_name().c_str()); + sensor->publish_state(NAN); this->status_set_warning(); return; } if (!sensor->check_scratch_pad()) { + ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", sensor->get_name().c_str()); + sensor->publish_state(NAN); this->status_set_warning(); return; } @@ -244,11 +248,7 @@ bool DallasTemperatureSensor::check_scratch_pad() { this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], crc8(this->scratch_pad_, 8)); #endif - if (crc8(this->scratch_pad_, 8) != this->scratch_pad_[8]) { - ESP_LOGE(TAG, "Reading scratchpad from Dallas Sensor failed"); - return false; - } - return true; + return crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]; } float DallasTemperatureSensor::get_temp_c() { int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3); diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 93ef04d195..5babf422bd 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation -from esphome.automation import maybe_simple_id from esphome.const import CONF_ID, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_CYCLES, \ CONF_RUN_DURATION, CONF_SLEEP_DURATION, CONF_WAKEUP_PIN @@ -85,7 +84,7 @@ def to_code(config): cg.add_define('USE_DEEP_SLEEP') -DEEP_SLEEP_ACTION_SCHEMA = maybe_simple_id({ +DEEP_SLEEP_ACTION_SCHEMA = automation.maybe_simple_id({ cv.GenerateID(): cv.use_id(DeepSleepComponent), }) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py new file mode 100644 index 0000000000..890c2bede4 --- /dev/null +++ b/esphome/components/dfplayer/__init__.py @@ -0,0 +1,219 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE +from esphome.components import uart + +DEPENDENCIES = ['uart'] + +dfplayer_ns = cg.esphome_ns.namespace('dfplayer') +DFPlayer = dfplayer_ns.class_('DFPlayer', cg.Component) +DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_('DFPlayerFinishedPlaybackTrigger', + automation.Trigger.template()) +DFPlayerIsPlayingCondition = dfplayer_ns.class_('DFPlayerIsPlayingCondition', automation.Condition) + +MULTI_CONF = True +CONF_FOLDER = 'folder' +CONF_LOOP = 'loop' +CONF_VOLUME = 'volume' +CONF_EQ_PRESET = 'eq_preset' +CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' + +EqPreset = dfplayer_ns.enum("EqPreset") +EQ_PRESET = { + 'NORMAL': EqPreset.NORMAL, + 'POP': EqPreset.POP, + 'ROCK': EqPreset.ROCK, + 'JAZZ': EqPreset.JAZZ, + 'CLASSIC': EqPreset.CLASSIC, + 'BASS': EqPreset.BASS, +} +Device = dfplayer_ns.enum("Device") +DEVICE = { + 'USB': Device.USB, + 'TF_CARD': Device.TF_CARD, +} + +NextAction = dfplayer_ns.class_('NextAction', automation.Action) +PreviousAction = dfplayer_ns.class_('PreviousAction', automation.Action) +PlayFileAction = dfplayer_ns.class_('PlayFileAction', automation.Action) +PlayFolderAction = dfplayer_ns.class_('PlayFolderAction', automation.Action) +SetVolumeAction = dfplayer_ns.class_('SetVolumeAction', automation.Action) +SetEqAction = dfplayer_ns.class_('SetEqAction', automation.Action) +SleepAction = dfplayer_ns.class_('SleepAction', automation.Action) +ResetAction = dfplayer_ns.class_('ResetAction', automation.Action) +StartAction = dfplayer_ns.class_('StartAction', automation.Action) +PauseAction = dfplayer_ns.class_('PauseAction', automation.Action) +StopAction = dfplayer_ns.class_('StopAction', automation.Action) +RandomAction = dfplayer_ns.class_('RandomAction', automation.Action) +SetDeviceAction = dfplayer_ns.class_('SetDeviceAction', automation.Action) + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(DFPlayer), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DFPlayerFinishedPlaybackTrigger), + }), +}).extend(uart.UART_DEVICE_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + + +@automation.register_action('dfplayer.play_next', NextAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_next_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.play_previous', PreviousAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_previous_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.play', PlayFileAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FILE): cv.templatable(cv.int_), + cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), +}, key=CONF_FILE)) +def dfplayer_play_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_FILE], args, float) + cg.add(var.set_file(template_)) + if CONF_LOOP in config: + template_ = yield cg.templatable(config[CONF_LOOP], args, float) + cg.add(var.set_loop(template_)) + yield var + + +@automation.register_action('dfplayer.play_folder', PlayFolderAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FOLDER): cv.templatable(cv.int_), + cv.Optional(CONF_FILE): cv.templatable(cv.int_), + cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), +})) +def dfplayer_play_folder_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_FOLDER], args, float) + cg.add(var.set_folder(template_)) + if CONF_FILE in config: + template_ = yield cg.templatable(config[CONF_FILE], args, float) + cg.add(var.set_file(template_)) + if CONF_LOOP in config: + template_ = yield cg.templatable(config[CONF_LOOP], args, float) + cg.add(var.set_loop(template_)) + yield var + + +@automation.register_action('dfplayer.set_device', SetDeviceAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_DEVICE): cv.enum(DEVICE, upper=True), +}, key=CONF_DEVICE)) +def dfplayer_set_device_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_DEVICE], args, Device) + cg.add(var.set_device(template_)) + yield var + + +@automation.register_action('dfplayer.set_volume', SetVolumeAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_VOLUME): cv.templatable(cv.int_), +}, key=CONF_VOLUME)) +def dfplayer_set_volume_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_VOLUME], args, float) + cg.add(var.set_volume(template_)) + yield var + + +@automation.register_action('dfplayer.set_eq', SetEqAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_EQ_PRESET): cv.templatable(cv.enum(EQ_PRESET, upper=True)), +}, key=CONF_EQ_PRESET)) +def dfplayer_set_eq_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_EQ_PRESET], args, EqPreset) + cg.add(var.set_eq(template_)) + yield var + + +@automation.register_action('dfplayer.sleep', SleepAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_sleep_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.reset', ResetAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.start', StartAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_start_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.pause', PauseAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_pause_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.stop', StopAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.random', RandomAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_random_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_condition('dfplayer.is_playing', DFPlayerIsPlayingCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp new file mode 100644 index 0000000000..5ce4998796 --- /dev/null +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -0,0 +1,124 @@ +#include "dfplayer.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace dfplayer { + +static const char* TAG = "dfplayer"; + +void DFPlayer::play_folder(uint16_t folder, uint16_t file) { + if (folder < 100 && file < 256) { + this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); + } else if (folder <= 10 && file <= 1000) { + this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); + } else { + ESP_LOGE(TAG, "Cannot play folder %d file %d.", folder, file); + } +} + +void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { + uint8_t buffer[10]{0x7e, 0xff, 0x06, cmd, 0x01, (uint8_t)(argument >> 8), (uint8_t) argument, 0x00, 0x00, 0xef}; + uint16_t checksum = 0; + for (uint8_t i = 1; i < 7; i++) + checksum += buffer[i]; + checksum = -checksum; + buffer[7] = checksum >> 8; + buffer[8] = (uint8_t) checksum; + + this->sent_cmd_ = cmd; + + ESP_LOGD(TAG, "Send Command %#02x arg %#04x", cmd, argument); + this->write_array(buffer, 10); +} + +void DFPlayer::loop() { + // Read message + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH) + this->read_pos_ = 0; + + switch (this->read_pos_) { + case 0: // Start mark + if (byte != 0x7E) + continue; + break; + case 1: // Version + if (byte != 0xFF) { + ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 2: // Buffer length + if (byte != 0x06) { + ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 9: // End byte + if (byte != 0xEF) { + ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + // Parse valid received command + uint8_t cmd = this->read_buffer_[3]; + uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6]; + + ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument); + + switch (cmd) { + case 0x3A: + if (argument == 1) { + ESP_LOGI(TAG, "USB loaded"); + } else if (argument == 2) + ESP_LOGI(TAG, "TF Card loaded"); + break; + case 0x3B: + if (argument == 1) { + ESP_LOGI(TAG, "USB unloaded"); + } else if (argument == 2) + ESP_LOGI(TAG, "TF Card unloaded"); + break; + case 0x3F: + if (argument == 1) { + ESP_LOGI(TAG, "USB available"); + } else if (argument == 2) { + ESP_LOGI(TAG, "TF Card available"); + } else if (argument == 3) { + ESP_LOGI(TAG, "USB, TF Card available"); + } + break; + case 0x41: + ESP_LOGV(TAG, "Ack ok"); + this->is_playing_ |= this->ack_set_is_playing_; + this->is_playing_ &= !this->ack_reset_is_playing_; + this->ack_set_is_playing_ = false; + this->ack_reset_is_playing_ = false; + break; + case 0x3D: // Playback finished + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); + break; + default: + ESP_LOGD(TAG, "Command %#02x arg %#04x", cmd, argument); + } + this->sent_cmd_ = 0; + this->read_pos_ = 0; + continue; + } + this->read_buffer_[this->read_pos_] = byte; + this->read_pos_++; + } +} +void DFPlayer::dump_config() { + ESP_LOGCONFIG(TAG, "DFPlayer:"); + this->check_uart_settings(9600); +} + +} // namespace dfplayer +} // namespace esphome diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h new file mode 100644 index 0000000000..86efd62138 --- /dev/null +++ b/esphome/components/dfplayer/dfplayer.h @@ -0,0 +1,166 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" + +const size_t DFPLAYER_READ_BUFFER_LENGTH = 25; // two messages + some extra + +namespace esphome { +namespace dfplayer { + +enum EqPreset { + NORMAL = 0, + POP = 1, + ROCK = 2, + JAZZ = 3, + CLASSIC = 4, + BASS = 5, +}; + +enum Device { + USB = 1, + TF_CARD = 2, +}; + +class DFPlayer : public uart::UARTDevice, public Component { + public: + void loop() override; + + void next() { this->send_cmd_(0x01); } + void previous() { this->send_cmd_(0x02); } + void play_file(uint16_t file) { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x03, file); + } + void play_file_loop(uint16_t file) { this->send_cmd_(0x08, file); } + void play_folder(uint16_t folder, uint16_t file); + void play_folder_loop(uint16_t folder) { this->send_cmd_(0x17, folder); } + void volume_up() { this->send_cmd_(0x04); } + void volume_down() { this->send_cmd_(0x05); } + void set_device(Device device) { this->send_cmd_(0x09, device); } + void set_volume(uint8_t volume) { this->send_cmd_(0x06, volume); } + void set_eq(EqPreset preset) { this->send_cmd_(0x07, preset); } + void sleep() { this->send_cmd_(0x0A); } + void reset() { this->send_cmd_(0x0C); } + void start() { this->send_cmd_(0x0D); } + void pause() { + this->ack_reset_is_playing_ = true; + this->send_cmd_(0x0E); + } + void stop() { this->send_cmd_(0x16); } + void random() { this->send_cmd_(0x18); } + + bool is_playing() { return is_playing_; } + void dump_config() override; + + void add_on_finished_playback_callback(std::function<void()> callback) { + this->on_finished_playback_callback_.add(std::move(callback)); + } + + protected: + void send_cmd_(uint8_t cmd, uint16_t argument = 0); + void send_cmd_(uint8_t cmd, uint16_t high, uint16_t low) { + this->send_cmd_(cmd, ((high & 0xFF) << 8) | (low & 0xFF)); + } + uint8_t sent_cmd_{0}; + + char read_buffer_[DFPLAYER_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + + bool is_playing_{false}; + bool ack_set_is_playing_{false}; + bool ack_reset_is_playing_{false}; + + CallbackManager<void()> on_finished_playback_callback_; +}; + +#define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \ + template<typename... Ts> class ACTION_CLASS : public Action<Ts...>, public Parented<DFPlayer> { \ + public: \ + void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ + }; + +DFPLAYER_SIMPLE_ACTION(NextAction, next) +DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) + +template<typename... Ts> class PlayFileAction : public Action<Ts...>, public Parented<DFPlayer> { + public: + TEMPLATABLE_VALUE(uint16_t, file) + TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { + auto file = this->file_.value(x...); + auto loop = this->loop_.value(x...); + if (loop) { + this->parent_->play_file_loop(file); + } else { + this->parent_->play_file(file); + } + } +}; + +template<typename... Ts> class PlayFolderAction : public Action<Ts...>, public Parented<DFPlayer> { + public: + TEMPLATABLE_VALUE(uint16_t, folder) + TEMPLATABLE_VALUE(uint16_t, file) + TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { + auto folder = this->folder_.value(x...); + auto file = this->file_.value(x...); + auto loop = this->loop_.value(x...); + if (loop) { + this->parent_->play_folder_loop(folder); + } else { + this->parent_->play_folder(folder, file); + } + } +}; + +template<typename... Ts> class SetDeviceAction : public Action<Ts...>, public Parented<DFPlayer> { + public: + TEMPLATABLE_VALUE(Device, device) + void play(Ts... x) override { + auto device = this->device_.value(x...); + this->parent_->set_device(device); + } +}; + +template<typename... Ts> class SetVolumeAction : public Action<Ts...>, public Parented<DFPlayer> { + public: + TEMPLATABLE_VALUE(uint8_t, volume) + void play(Ts... x) override { + auto volume = this->volume_.value(x...); + this->parent_->set_volume(volume); + } +}; + +template<typename... Ts> class SetEqAction : public Action<Ts...>, public Parented<DFPlayer> { + public: + TEMPLATABLE_VALUE(EqPreset, eq) + void play(Ts... x) override { + auto eq = this->eq_.value(x...); + this->parent_->set_eq(eq); + } +}; + +DFPLAYER_SIMPLE_ACTION(SleepAction, sleep) +DFPLAYER_SIMPLE_ACTION(ResetAction, reset) +DFPLAYER_SIMPLE_ACTION(StartAction, start) +DFPLAYER_SIMPLE_ACTION(PauseAction, pause) +DFPLAYER_SIMPLE_ACTION(StopAction, stop) +DFPLAYER_SIMPLE_ACTION(RandomAction, random) + +template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts...>, public Parented<DFPlayer> { + public: + bool check(Ts... x) override { return this->parent_->is_playing(); } +}; + +class DFPlayerFinishedPlaybackTrigger : public Trigger<> { + public: + explicit DFPlayerFinishedPlaybackTrigger(DFPlayer *parent) { + parent->add_on_finished_playback_callback([this]() { this->trigger(); }); + } +}; + +} // namespace dfplayer +} // namespace esphome diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 797137c08c..1e28246bee 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -56,6 +56,8 @@ void DHT::update() { str = " and consider manually specifying the DHT model using the model option"; } ESP_LOGW(TAG, "Invalid readings! Please check your wiring (pull-up resistor, pin number)%s.", str); + this->temperature_sensor_->publish_state(NAN); + this->humidity_sensor_->publish_state(NAN); this->status_set_warning(); } } @@ -153,13 +155,15 @@ bool HOT DHT::read_sensor_(float *temperature, float *humidity, bool report_erro if (checksum_a != data[4] && checksum_b != data[4]) { if (report_errors) { - ESP_LOGE(TAG, "Checksum invalid: %u!=%u", checksum_a, data[4]); + ESP_LOGW(TAG, "Checksum invalid: %u!=%u", checksum_a, data[4]); } return false; } if (this->model_ == DHT_MODEL_DHT11) { *humidity = data[0]; + if (*humidity > 100) + *humidity = NAN; *temperature = data[2]; } else { uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); @@ -170,18 +174,20 @@ bool HOT DHT::read_sensor_(float *temperature, float *humidity, bool report_erro if (raw_temperature == 1 && raw_humidity == 10) { if (report_errors) { - ESP_LOGE(TAG, "Invalid temperature+humidity! Sensor reported 1°C and 1%% Hum"); + ESP_LOGW(TAG, "Invalid temperature+humidity! Sensor reported 1°C and 1%% Hum"); } return false; } *humidity = raw_humidity * 0.1f; + if (*humidity > 100) + *humidity = NAN; *temperature = int16_t(raw_temperature) * 0.1f; } if (*temperature == 0.0f && (*humidity == 1.0f || *humidity == 2.0f)) { if (report_errors) { - ESP_LOGE(TAG, "DHT reports invalid data. Is the update interval too high or the sensor damaged?"); + ESP_LOGW(TAG, "DHT reports invalid data. Is the update interval too high or the sensor damaged?"); } return false; } diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index e1e18bb7f9..8455f74fb4 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import sensor from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_MODEL, CONF_PIN, CONF_TEMPERATURE, \ - CONF_UPDATE_INTERVAL, ICON_THERMOMETER, UNIT_CELSIUS, ICON_WATER_PERCENT, UNIT_PERCENT + ICON_THERMOMETER, UNIT_CELSIUS, ICON_WATER_PERCENT, UNIT_PERCENT from esphome.cpp_helpers import gpio_pin_expression dht_ns = cg.esphome_ns.namespace('dht') @@ -24,7 +24,6 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), cv.Optional(CONF_MODEL, default='auto detect'): cv.enum(DHT_MODELS, upper=True, space='_'), - cv.Optional(CONF_UPDATE_INTERVAL, default='60s'): cv.update_interval, }).extend(cv.polling_component_schema('60s')) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 5c204fc7a4..38d19d832e 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import core, automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_ROTATION, CONF_UPDATE_INTERVAL +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_ROTATION from esphome.core import coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -33,7 +33,6 @@ def validate_rotation(value): BASIC_DISPLAY_SCHEMA = cv.Schema({ - cv.Optional(CONF_UPDATE_INTERVAL): cv.update_interval, cv.Optional(CONF_LAMBDA): cv.lambda_, }) @@ -48,8 +47,6 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend({ @coroutine def setup_display_core_(var, config): - if CONF_UPDATE_INTERVAL in config: - cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) if CONF_ROTATION in config: cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) if CONF_PAGES in config: diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 57b95eee29..b12fad8c8a 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -84,8 +84,8 @@ using display_writer_t = std::function<void(DisplayBuffer &)>; #define LOG_DISPLAY(prefix, type, obj) \ if (obj != nullptr) { \ ESP_LOGCONFIG(TAG, prefix type); \ - ESP_LOGCONFIG(TAG, prefix " Rotations: %d °", obj->rotation_); \ - ESP_LOGCONFIG(TAG, prefix " Dimensions: %dpx x %dpx", obj->get_width(), obj->get_height()); \ + ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, obj->rotation_); \ + ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, obj->get_width(), obj->get_height()); \ } class DisplayBuffer { diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 1f3bf01a86..b7810bd056 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -31,6 +31,11 @@ static esp_ble_adv_params_t ble_adv_params = { static esp_ble_ibeacon_head_t ibeacon_common_head = { .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = 0x004C, .beacon_type = 0x1502}; +void ESP32BLEBeacon::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 BLE Beacon:"); + ESP_LOGCONFIG(TAG, " Major: %u, Minor: %u", this->major_, this->minor_); +} + void ESP32BLEBeacon::setup() { ESP_LOGCONFIG(TAG, "Setting up ESP32 BLE beacon..."); global_esp32_ble_beacon = this; @@ -50,7 +55,7 @@ void ESP32BLEBeacon::ble_core_task(void *params) { ble_setup(); while (true) { - delay(1000); + delay(1000); // NOLINT } } void ESP32BLEBeacon::ble_setup() { diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 1c52b41d73..aba02830b3 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -34,6 +34,7 @@ class ESP32BLEBeacon : public Component { explicit ESP32BLEBeacon(const std::array<uint8_t, 16> &uuid) : uuid_(uuid) {} void setup() override; + void dump_config() override; float get_setup_priority() const override; void set_major(uint16_t major) { this->major_ = major; } diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 5a4862f733..7e998e77b1 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,19 +1,48 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SCAN_INTERVAL, ESP_PLATFORM_ESP32 +from esphome.const import CONF_ID, ESP_PLATFORM_ESP32, CONF_INTERVAL, \ + CONF_DURATION from esphome.core import coroutine ESP_PLATFORMS = [ESP_PLATFORM_ESP32] AUTO_LOAD = ['xiaomi_ble'] CONF_ESP32_BLE_ID = 'esp32_ble_id' +CONF_SCAN_PARAMETERS = 'scan_parameters' +CONF_WINDOW = 'window' +CONF_ACTIVE = 'active' esp32_ble_tracker_ns = cg.esphome_ns.namespace('esp32_ble_tracker') ESP32BLETracker = esp32_ble_tracker_ns.class_('ESP32BLETracker', cg.Component) ESPBTDeviceListener = esp32_ble_tracker_ns.class_('ESPBTDeviceListener') + +def validate_scan_parameters(config): + duration = config[CONF_DURATION] + interval = config[CONF_INTERVAL] + window = config[CONF_WINDOW] + + if window > interval: + raise cv.Invalid("Scan window ({}) needs to be smaller than scan interval ({})" + "".format(window, interval)) + + if interval.total_milliseconds * 3 > duration.total_milliseconds: + raise cv.Invalid("Scan duration needs to be at least three times the scan interval to" + "cover all BLE channels.") + + return config + + CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(ESP32BLETracker), - cv.Optional(CONF_SCAN_INTERVAL, default='300s'): cv.positive_time_period_seconds, + cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(cv.Schema({ + cv.Optional(CONF_DURATION, default='5min'): cv.positive_time_period_seconds, + cv.Optional(CONF_INTERVAL, default='320ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_WINDOW, default='200ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ACTIVE, default=True): cv.boolean, + }), validate_scan_parameters), + + cv.Optional('scan_interval'): cv.invalid("This option has been removed in 1.14 (Reason: " + "it never had an effect)"), }).extend(cv.COMPONENT_SCHEMA) ESP_BLE_DEVICE_SCHEMA = cv.Schema({ @@ -24,7 +53,11 @@ ESP_BLE_DEVICE_SCHEMA = cv.Schema({ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) - cg.add(var.set_scan_interval(config[CONF_SCAN_INTERVAL])) + params = config[CONF_SCAN_PARAMETERS] + cg.add(var.set_scan_duration(params[CONF_DURATION])) + cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625))) + cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625))) + cg.add(var.set_scan_active(params[CONF_ACTIVE])) @coroutine diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 3aaf8eaa44..7a5bd733a2 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -1,6 +1,7 @@ #include "esp32_ble_tracker.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #ifdef ARDUINO_ARCH_ESP32 @@ -133,14 +134,14 @@ bool ESP32BLETracker::ble_setup() { } // BLE takes some time to be fully set up, 200ms should be more than enough - delay(200); + delay(200); // NOLINT return true; } void ESP32BLETracker::start_scan(bool first) { if (!xSemaphoreTake(this->scan_end_lock_, 0L)) { - ESP_LOGW("Cannot start scan!"); + ESP_LOGW(TAG, "Cannot start scan!"); return; } @@ -150,21 +151,16 @@ void ESP32BLETracker::start_scan(bool first) { listener->on_scan_end(); } this->already_discovered_.clear(); - this->scan_params_.scan_type = BLE_SCAN_TYPE_ACTIVE; + this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE; this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; - // Values determined empirically, higher scan intervals and lower scan windows make the ESP more stable - // Ideally, these values should both be quite low, especially scan window. 0x10/0x10 is the esp-idf - // default and works quite well. 0x100/0x50 discovers a few less BLE broadcast packets but is a lot - // more stable (order of several hours). The old ESPHome default (1600/1600) was terrible with - // crashes every few minutes - this->scan_params_.scan_interval = 0x200; - this->scan_params_.scan_window = 0x30; + this->scan_params_.scan_interval = this->scan_interval_; + this->scan_params_.scan_window = this->scan_window_; esp_ble_gap_set_scan_params(&this->scan_params_); - esp_ble_gap_start_scanning(this->scan_interval_); + esp_ble_gap_start_scanning(this->scan_duration_); - this->set_timeout("scan", this->scan_interval_ * 2000, []() { + this->set_timeout("scan", this->scan_duration_ * 2000, []() { ESP_LOGW(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack..."); App.reboot(); }); @@ -207,20 +203,8 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res } } -std::string hexencode(const std::string &raw_data) { - char buf[20]; - std::string res; - for (size_t i = 0; i < raw_data.size(); i++) { - if (i + 1 != raw_data.size()) { - sprintf(buf, "0x%02X.", static_cast<uint8_t>(raw_data[i])); - } else { - sprintf(buf, "0x%02X ", static_cast<uint8_t>(raw_data[i])); - } - res += buf; - } - sprintf(buf, "(%zu)", raw_data.size()); - res += buf; - return res; +std::string hexencode_string(const std::string &raw_data) { + return hexencode(reinterpret_cast<const uint8_t *>(raw_data.c_str()), raw_data.size()); } ESPBTUUID::ESPBTUUID() : uuid_() {} @@ -332,15 +316,15 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e for (auto uuid : this->service_uuids_) { ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); } - ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(this->manufacturer_data_).c_str()); - ESP_LOGVV(TAG, " Service data: %s", hexencode(this->service_data_).c_str()); + ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode_string(this->manufacturer_data_).c_str()); + ESP_LOGVV(TAG, " Service data: %s", hexencode_string(this->service_data_).c_str()); if (this->service_data_uuid_.has_value()) { ESP_LOGVV(TAG, " Service Data UUID: %s", this->service_data_uuid_->to_string().c_str()); } ESP_LOGVV(TAG, "Adv data: %s", - hexencode(std::string(reinterpret_cast<const char *>(param.ble_adv), param.adv_data_len)).c_str()); + hexencode_string(std::string(reinterpret_cast<const char *>(param.ble_adv), param.adv_data_len)).c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { @@ -454,10 +438,12 @@ const std::string &ESPBTDevice::get_manufacturer_data() const { return this->man const std::string &ESPBTDevice::get_service_data() const { return this->service_data_; } const optional<ESPBTUUID> &ESPBTDevice::get_service_data_uuid() const { return this->service_data_uuid_; } -void ESP32BLETracker::set_scan_interval(uint32_t scan_interval) { this->scan_interval_ = scan_interval; } void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); - ESP_LOGCONFIG(TAG, " Scan Interval: %u s", this->scan_interval_); + ESP_LOGCONFIG(TAG, " Scan Duration: %u s", this->scan_duration_); + ESP_LOGCONFIG(TAG, " Scan Interval: %u ms", this->scan_interval_); + ESP_LOGCONFIG(TAG, " Scan Window: %u ms", this->scan_window_); + ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE"); } void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { const uint64_t address = device.address_uint64(); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f1bcada621..82e8e553fc 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -107,7 +107,10 @@ class ESPBTDeviceListener { class ESP32BLETracker : public Component { public: - void set_scan_interval(uint32_t scan_interval); + void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } + void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } + void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; } + void set_scan_active(bool scan_active) { scan_active_ = scan_active; } /// Setup the FreeRTOS task and the Bluetooth stack. void setup() override; @@ -142,7 +145,10 @@ class ESP32BLETracker : public Component { /// A structure holding the ESP BLE scan parameters. esp_ble_scan_params_t scan_params_; /// The interval in seconds to perform scans. - uint32_t scan_interval_{300}; + uint32_t scan_duration_; + uint32_t scan_interval_; + uint32_t scan_window_; + bool scan_active_; SemaphoreHandle_t scan_result_lock_; SemaphoreHandle_t scan_end_lock_; size_t scan_result_index_{0}; diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index c69e0a5710..81980d9d38 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NAME, CONF_PIN, CONF_SCL, CONF_SDA, \ - ESP_PLATFORM_ESP32 + ESP_PLATFORM_ESP32, CONF_DATA_PINS, CONF_RESET_PIN, CONF_RESOLUTION, CONF_BRIGHTNESS ESP_PLATFORMS = [ESP_PLATFORM_ESP32] DEPENDENCIES = ['api'] @@ -35,23 +35,19 @@ FRAME_SIZES = { 'UXGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, } -CONF_DATA_PINS = 'data_pins' CONF_VSYNC_PIN = 'vsync_pin' CONF_HREF_PIN = 'href_pin' CONF_PIXEL_CLOCK_PIN = 'pixel_clock_pin' CONF_EXTERNAL_CLOCK = 'external_clock' CONF_I2C_PINS = 'i2c_pins' -CONF_RESET_PIN = 'reset_pin' CONF_POWER_DOWN_PIN = 'power_down_pin' CONF_MAX_FRAMERATE = 'max_framerate' CONF_IDLE_FRAMERATE = 'idle_framerate' -CONF_RESOLUTION = 'resolution' CONF_JPEG_QUALITY = 'jpeg_quality' CONF_VERTICAL_FLIP = 'vertical_flip' CONF_HORIZONTAL_MIRROR = 'horizontal_mirror' CONF_CONTRAST = 'contrast' -CONF_BRIGHTNESS = 'brightness' CONF_SATURATION = 'saturation' CONF_TEST_PATTERN = 'test_pattern' diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e85d0ef5c2..ce0028159e 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -108,6 +108,8 @@ void ESP32TouchComponent::dump_config() { } void ESP32TouchComponent::loop() { + const uint32_t now = millis(); + bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; for (auto *child : this->children_) { uint16_t value; if (this->iir_filter_enabled_()) { @@ -116,35 +118,20 @@ void ESP32TouchComponent::loop() { touch_pad_read(child->get_touch_pad(), &value); } + child->value_ = value; child->publish_state(value < child->get_threshold()); - if (this->setup_mode_) { + if (should_print) { ESP_LOGD(TAG, "Touch Pad '%s' (T%u): %u", child->get_name().c_str(), child->get_touch_pad(), value); } } - if (this->setup_mode_) { + if (should_print) { // Avoid spamming logs - delay(250); + this->setup_mode_last_log_print_ = now; } } -void ESP32TouchComponent::register_touch_pad(ESP32TouchBinarySensor *pad) { this->children_.push_back(pad); } -void ESP32TouchComponent::set_setup_mode(bool setup_mode) { this->setup_mode_ = setup_mode; } -bool ESP32TouchComponent::iir_filter_enabled_() const { return this->iir_filter_ > 0; } -void ESP32TouchComponent::set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; } -float ESP32TouchComponent::get_setup_priority() const { return setup_priority::DATA; } -void ESP32TouchComponent::set_sleep_duration(uint16_t sleep_duration) { this->sleep_cycle_ = sleep_duration; } -void ESP32TouchComponent::set_measurement_duration(uint16_t meas_cycle) { this->meas_cycle_ = meas_cycle; } -void ESP32TouchComponent::set_low_voltage_reference(touch_low_volt_t low_voltage_reference) { - this->low_voltage_reference_ = low_voltage_reference; -} -void ESP32TouchComponent::set_high_voltage_reference(touch_high_volt_t high_voltage_reference) { - this->high_voltage_reference_ = high_voltage_reference; -} -void ESP32TouchComponent::set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { - this->voltage_attenuation_ = voltage_attenuation; -} void ESP32TouchComponent::on_shutdown() { if (this->iir_filter_enabled_()) { touch_pad_filter_stop(); @@ -155,8 +142,6 @@ void ESP32TouchComponent::on_shutdown() { ESP32TouchBinarySensor::ESP32TouchBinarySensor(const std::string &name, touch_pad_t touch_pad, uint16_t threshold) : BinarySensor(name), touch_pad_(touch_pad), threshold_(threshold) {} -touch_pad_t ESP32TouchBinarySensor::get_touch_pad() const { return this->touch_pad_; } -uint16_t ESP32TouchBinarySensor::get_threshold() const { return this->threshold_; } } // namespace esp32_touch } // namespace esphome diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index b68876c33e..45d459a2ff 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -12,32 +12,36 @@ class ESP32TouchBinarySensor; class ESP32TouchComponent : public Component { public: - void register_touch_pad(ESP32TouchBinarySensor *pad); + void register_touch_pad(ESP32TouchBinarySensor *pad) { children_.push_back(pad); } - void set_setup_mode(bool setup_mode); + void set_setup_mode(bool setup_mode) { setup_mode_ = setup_mode; } - void set_iir_filter(uint32_t iir_filter); + void set_iir_filter(uint32_t iir_filter) { iir_filter_ = iir_filter; } - void set_sleep_duration(uint16_t sleep_duration); + void set_sleep_duration(uint16_t sleep_duration) { sleep_cycle_ = sleep_duration; } - void set_measurement_duration(uint16_t meas_cycle); + void set_measurement_duration(uint16_t meas_cycle) { meas_cycle_ = meas_cycle; } - void set_low_voltage_reference(touch_low_volt_t low_voltage_reference); + void set_low_voltage_reference(touch_low_volt_t low_voltage_reference) { + low_voltage_reference_ = low_voltage_reference; + } - void set_high_voltage_reference(touch_high_volt_t high_voltage_reference); + void set_high_voltage_reference(touch_high_volt_t high_voltage_reference) { + high_voltage_reference_ = high_voltage_reference; + } - void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation); + void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { voltage_attenuation_ = voltage_attenuation; } void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::DATA; } void on_shutdown() override; protected: /// Is the IIR filter enabled? - bool iir_filter_enabled_() const; + bool iir_filter_enabled_() const { return iir_filter_ > 0; } uint16_t sleep_cycle_{}; uint16_t meas_cycle_{65535}; @@ -46,6 +50,7 @@ class ESP32TouchComponent : public Component { touch_volt_atten_t voltage_attenuation_{}; std::vector<ESP32TouchBinarySensor *> children_; bool setup_mode_{false}; + uint32_t setup_mode_last_log_print_{}; uint32_t iir_filter_{0}; }; @@ -54,14 +59,17 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { public: ESP32TouchBinarySensor(const std::string &name, touch_pad_t touch_pad, uint16_t threshold); - touch_pad_t get_touch_pad() const; - uint16_t get_threshold() const; + touch_pad_t get_touch_pad() const { return touch_pad_; } + uint16_t get_threshold() const { return threshold_; } + void set_threshold(uint16_t threshold) { threshold_ = threshold; } + uint16_t get_value() const { return value_; } protected: friend ESP32TouchComponent; touch_pad_t touch_pad_; uint16_t threshold_; + uint16_t value_; }; } // namespace esp32_touch diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index 7354f9ae9f..b552c917c0 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -34,5 +34,6 @@ def new_fastled_light(config): cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) yield light.register_light(var, config) - cg.add_library('FastLED', '3.2.0') + # https://github.com/FastLED/FastLED/blob/master/library.json + cg.add_library('FastLED', '3.2.9') yield var diff --git a/esphome/components/fujitsu_general/__init__.py b/esphome/components/fujitsu_general/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/fujitsu_general/climate.py b/esphome/components/fujitsu_general/climate.py new file mode 100644 index 0000000000..a6774c397a --- /dev/null +++ b/esphome/components/fujitsu_general/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +fujitsu_general_ns = cg.esphome_ns.namespace('fujitsu_general') +FujitsuGeneralClimate = fujitsu_general_ns.class_('FujitsuGeneralClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(FujitsuGeneralClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp new file mode 100644 index 0000000000..261d8be258 --- /dev/null +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -0,0 +1,212 @@ +#include "fujitsu_general.h" + +namespace esphome { +namespace fujitsu_general { + +static const char *TAG = "fujitsu_general.climate"; + +// Control packet +const uint16_t FUJITSU_GENERAL_STATE_LENGTH = 16; + +const uint8_t FUJITSU_GENERAL_BASE_BYTE0 = 0x14; +const uint8_t FUJITSU_GENERAL_BASE_BYTE1 = 0x63; +const uint8_t FUJITSU_GENERAL_BASE_BYTE2 = 0x00; +const uint8_t FUJITSU_GENERAL_BASE_BYTE3 = 0x10; +const uint8_t FUJITSU_GENERAL_BASE_BYTE4 = 0x10; +const uint8_t FUJITSU_GENERAL_BASE_BYTE5 = 0xFE; +const uint8_t FUJITSU_GENERAL_BASE_BYTE6 = 0x09; +const uint8_t FUJITSU_GENERAL_BASE_BYTE7 = 0x30; + +// Temperature and POWER ON +const uint8_t FUJITSU_GENERAL_POWER_ON_MASK_BYTE8 = 0b00000001; +const uint8_t FUJITSU_GENERAL_BASE_BYTE8 = 0x40; + +// Mode +const uint8_t FUJITSU_GENERAL_MODE_AUTO_BYTE9 = 0x00; +const uint8_t FUJITSU_GENERAL_MODE_HEAT_BYTE9 = 0x04; +const uint8_t FUJITSU_GENERAL_MODE_COOL_BYTE9 = 0x01; +const uint8_t FUJITSU_GENERAL_MODE_DRY_BYTE9 = 0x02; +const uint8_t FUJITSU_GENERAL_MODE_FAN_BYTE9 = 0x03; +const uint8_t FUJITSU_GENERAL_MODE_10C_BYTE9 = 0x0B; +const uint8_t FUJITSU_GENERAL_BASE_BYTE9 = 0x01; + +// Fan speed and swing +const uint8_t FUJITSU_GENERAL_FAN_AUTO_BYTE10 = 0x00; +const uint8_t FUJITSU_GENERAL_FAN_HIGH_BYTE10 = 0x01; +const uint8_t FUJITSU_GENERAL_FAN_MEDIUM_BYTE10 = 0x02; +const uint8_t FUJITSU_GENERAL_FAN_LOW_BYTE10 = 0x03; +const uint8_t FUJITSU_GENERAL_FAN_SILENT_BYTE10 = 0x04; +const uint8_t FUJITSU_GENERAL_SWING_MASK_BYTE10 = 0b00010000; +const uint8_t FUJITSU_GENERAL_BASE_BYTE10 = 0x00; + +const uint8_t FUJITSU_GENERAL_BASE_BYTE11 = 0x00; +const uint8_t FUJITSU_GENERAL_BASE_BYTE12 = 0x00; +const uint8_t FUJITSU_GENERAL_BASE_BYTE13 = 0x00; + +// Outdoor Unit Low Noise +const uint8_t FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14 = 0xA0; +const uint8_t FUJITSU_GENERAL_BASE_BYTE14 = 0x20; + +// CRC +const uint8_t FUJITSU_GENERAL_BASE_BYTE15 = 0x6F; + +// Power off packet is specific +const uint16_t FUJITSU_GENERAL_OFF_LENGTH = 7; + +const uint8_t FUJITSU_GENERAL_OFF_BYTE0 = FUJITSU_GENERAL_BASE_BYTE0; +const uint8_t FUJITSU_GENERAL_OFF_BYTE1 = FUJITSU_GENERAL_BASE_BYTE1; +const uint8_t FUJITSU_GENERAL_OFF_BYTE2 = FUJITSU_GENERAL_BASE_BYTE2; +const uint8_t FUJITSU_GENERAL_OFF_BYTE3 = FUJITSU_GENERAL_BASE_BYTE3; +const uint8_t FUJITSU_GENERAL_OFF_BYTE4 = FUJITSU_GENERAL_BASE_BYTE4; +const uint8_t FUJITSU_GENERAL_OFF_BYTE5 = 0x02; +const uint8_t FUJITSU_GENERAL_OFF_BYTE6 = 0xFD; + +const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius +const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius + +const uint16_t FUJITSU_GENERAL_HEADER_MARK = 3300; +const uint16_t FUJITSU_GENERAL_HEADER_SPACE = 1600; +const uint16_t FUJITSU_GENERAL_BIT_MARK = 420; +const uint16_t FUJITSU_GENERAL_ONE_SPACE = 1200; +const uint16_t FUJITSU_GENERAL_ZERO_SPACE = 420; +const uint16_t FUJITSU_GENERAL_TRL_MARK = 420; +const uint16_t FUJITSU_GENERAL_TRL_SPACE = 8000; + +const uint32_t FUJITSU_GENERAL_CARRIER_FREQUENCY = 38000; + +FujitsuGeneralClimate::FujitsuGeneralClimate() : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1) {} + +void FujitsuGeneralClimate::transmit_state() { + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->transmit_off_(); + return; + } + uint8_t remote_state[FUJITSU_GENERAL_STATE_LENGTH] = {0}; + + remote_state[0] = FUJITSU_GENERAL_BASE_BYTE0; + remote_state[1] = FUJITSU_GENERAL_BASE_BYTE1; + remote_state[2] = FUJITSU_GENERAL_BASE_BYTE2; + remote_state[3] = FUJITSU_GENERAL_BASE_BYTE3; + remote_state[4] = FUJITSU_GENERAL_BASE_BYTE4; + remote_state[5] = FUJITSU_GENERAL_BASE_BYTE5; + remote_state[6] = FUJITSU_GENERAL_BASE_BYTE6; + remote_state[7] = FUJITSU_GENERAL_BASE_BYTE7; + remote_state[8] = FUJITSU_GENERAL_BASE_BYTE8; + remote_state[9] = FUJITSU_GENERAL_BASE_BYTE9; + remote_state[10] = FUJITSU_GENERAL_BASE_BYTE10; + remote_state[11] = FUJITSU_GENERAL_BASE_BYTE11; + remote_state[12] = FUJITSU_GENERAL_BASE_BYTE12; + remote_state[13] = FUJITSU_GENERAL_BASE_BYTE13; + remote_state[14] = FUJITSU_GENERAL_BASE_BYTE14; + remote_state[15] = FUJITSU_GENERAL_BASE_BYTE15; + + // Set temperature + uint8_t safecelsius = std::max((uint8_t) this->target_temperature, FUJITSU_GENERAL_TEMP_MIN); + safecelsius = std::min(safecelsius, FUJITSU_GENERAL_TEMP_MAX); + remote_state[8] = (byte) safecelsius - 16; + remote_state[8] = remote_state[8] << 4; + + // If not powered - set power on flag + if (!this->power_) { + remote_state[8] = (byte) remote_state[8] | FUJITSU_GENERAL_POWER_ON_MASK_BYTE8; + } + + // Set mode + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state[9] = FUJITSU_GENERAL_MODE_COOL_BYTE9; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[9] = FUJITSU_GENERAL_MODE_HEAT_BYTE9; + break; + case climate::CLIMATE_MODE_AUTO: + default: + remote_state[9] = FUJITSU_GENERAL_MODE_AUTO_BYTE9; + break; + // TODO: CLIMATE_MODE_FAN_ONLY, CLIMATE_MODE_DRY, CLIMATE_MODE_10C are missing in esphome + } + + // TODO: missing support for fan speed + remote_state[10] = FUJITSU_GENERAL_FAN_AUTO_BYTE10; + + // TODO: missing support for swing + // remote_state[10] = (byte) remote_state[10] | FUJITSU_GENERAL_SWING_MASK_BYTE10; + + // TODO: missing support for outdoor unit low noise + // remote_state[14] = (byte) remote_state[14] | FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14; + + // CRC + remote_state[15] = 0; + for (int i = 7; i < 15; i++) { + remote_state[15] += (byte) remote_state[i]; // Addiction + } + remote_state[15] = 0x100 - remote_state[15]; // mod 256 + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(FUJITSU_GENERAL_CARRIER_FREQUENCY); + + // Header + data->mark(FUJITSU_GENERAL_HEADER_MARK); + data->space(FUJITSU_GENERAL_HEADER_SPACE); + // Data + for (uint8_t i : remote_state) { + // Send all Bits from Byte Data in Reverse Order + for (uint8_t mask = 00000001; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(FUJITSU_GENERAL_BIT_MARK); + bool bit = i & mask; + data->space(bit ? FUJITSU_GENERAL_ONE_SPACE : FUJITSU_GENERAL_ZERO_SPACE); + // Next bits + } + } + // Footer + data->mark(FUJITSU_GENERAL_TRL_MARK); + data->space(FUJITSU_GENERAL_TRL_SPACE); + + transmit.perform(); + + this->power_ = true; +} + +void FujitsuGeneralClimate::transmit_off_() { + uint8_t remote_state[FUJITSU_GENERAL_OFF_LENGTH] = {0}; + + remote_state[0] = FUJITSU_GENERAL_OFF_BYTE0; + remote_state[1] = FUJITSU_GENERAL_OFF_BYTE1; + remote_state[2] = FUJITSU_GENERAL_OFF_BYTE2; + remote_state[3] = FUJITSU_GENERAL_OFF_BYTE3; + remote_state[4] = FUJITSU_GENERAL_OFF_BYTE4; + remote_state[5] = FUJITSU_GENERAL_OFF_BYTE5; + remote_state[6] = FUJITSU_GENERAL_OFF_BYTE6; + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(FUJITSU_GENERAL_CARRIER_FREQUENCY); + + // Header + data->mark(FUJITSU_GENERAL_HEADER_MARK); + data->space(FUJITSU_GENERAL_HEADER_SPACE); + + // Data + for (uint8_t i : remote_state) { + // Send all Bits from Byte Data in Reverse Order + for (uint8_t mask = 00000001; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(FUJITSU_GENERAL_BIT_MARK); + bool bit = i & mask; + data->space(bit ? FUJITSU_GENERAL_ONE_SPACE : FUJITSU_GENERAL_ZERO_SPACE); + // Next bits + } + } + // Footer + data->mark(FUJITSU_GENERAL_TRL_MARK); + data->space(FUJITSU_GENERAL_TRL_SPACE); + + transmit.perform(); + + this->power_ = false; +} + +} // namespace fujitsu_general +} // namespace esphome diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h new file mode 100644 index 0000000000..80db81a167 --- /dev/null +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace fujitsu_general { + +class FujitsuGeneralClimate : public climate_ir::ClimateIR { + public: + FujitsuGeneralClimate(); + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Transmit via IR power off command. + void transmit_off_(); + + bool power_{false}; +}; + +} // namespace fujitsu_general +} // namespace esphome diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index c7d2a18d84..397c55f6c4 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/helpers.h" namespace esphome { namespace globals { @@ -64,5 +65,7 @@ template<class C, typename... Ts> class GlobalVarSetAction : public Action<Ts... C *parent_; }; +template<typename T> T &id(GlobalsComponent<T> *value) { return value->value(); } + } // namespace globals } // namespace esphome diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py index 7b383cb8a9..f75bc71009 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -15,12 +15,14 @@ RESTORE_MODES = { 'ALWAYS_ON': GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_ON, } +CONF_INTERLOCK_WAIT_TIME = 'interlock_wait_time' CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(GPIOSwitch), cv.Required(CONF_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESTORE_MODE, default='RESTORE_DEFAULT_OFF'): cv.enum(RESTORE_MODES, upper=True, space='_'), cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), + cv.Optional(CONF_INTERLOCK_WAIT_TIME, default='0ms'): cv.positive_time_period_milliseconds, }).extend(cv.COMPONENT_SCHEMA) @@ -40,3 +42,4 @@ def to_code(config): lock = yield cg.get_variable(it) interlock.append(lock) cg.add(var.set_interlock(interlock)) + cg.add(var.set_interlock_wait_time(config[CONF_INTERLOCK_WAIT_TIME])) diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index d22a74847e..d87e5a61e6 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -69,13 +69,29 @@ void GPIOSwitch::dump_config() { void GPIOSwitch::write_state(bool state) { if (state != this->inverted_) { // Turning ON, check interlocking + + bool found = false; for (auto *lock : this->interlock_) { if (lock == this) continue; - if (lock->state) + if (lock->state) { lock->turn_off(); + found = true; + } } + if (found && this->interlock_wait_time_ != 0) { + this->set_timeout("interlock", this->interlock_wait_time_, [this, state] { + // Don't write directly, call the function again + // (some other switch may have changed state while we were waiting) + this->write_state(state); + }); + return; + } + } else if (this->interlock_wait_time_ != 0) { + // If we are switched off during the interlock wait time, cancel any pending + // re-activations + this->cancel_timeout("interlock"); } this->pin_->digital_write(state); diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index ceace477b2..dc0dd9bc95 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -26,6 +26,7 @@ class GPIOSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; void set_interlock(const std::vector<Switch *> &interlock); + void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } protected: void write_state(bool state) override; @@ -33,6 +34,7 @@ class GPIOSwitch : public switch_::Switch, public Component { GPIOPin *pin_; GPIOSwitchRestoreMode restore_mode_{GPIO_SWITCH_RESTORE_DEFAULT_OFF}; std::vector<Switch *> interlock_; + uint32_t interlock_wait_time_{0}; }; } // namespace gpio diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 3ecbc89f73..ddbd29d5f8 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -20,4 +20,6 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) yield uart.register_uart_device(var, config) - cg.add_library('TinyGPSPlus', '1.0.2') + + # https://platformio.org/lib/show/1655/TinyGPSPlus + cg.add_library('1655', '1.0.2') # TinyGPSPlus, has name conflict diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index 0391a9a955..26371565f3 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -8,5 +8,41 @@ static const char *TAG = "gps"; TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); } +void GPS::loop() { + while (this->available() && !this->has_time_) { + if (this->tiny_gps_.encode(this->read())) { + if (tiny_gps_.location.isUpdated()) { + ESP_LOGD(TAG, "Location:"); + ESP_LOGD(TAG, " Lat: %f", tiny_gps_.location.lat()); + ESP_LOGD(TAG, " Lon: %f", tiny_gps_.location.lng()); + } + + if (tiny_gps_.speed.isUpdated()) { + ESP_LOGD(TAG, "Speed:"); + ESP_LOGD(TAG, " %f km/h", tiny_gps_.speed.kmph()); + } + if (tiny_gps_.course.isUpdated()) { + ESP_LOGD(TAG, "Course:"); + ESP_LOGD(TAG, " %f °", tiny_gps_.course.deg()); + } + if (tiny_gps_.altitude.isUpdated()) { + ESP_LOGD(TAG, "Altitude:"); + ESP_LOGD(TAG, " %f m", tiny_gps_.altitude.meters()); + } + if (tiny_gps_.satellites.isUpdated()) { + ESP_LOGD(TAG, "Satellites:"); + ESP_LOGD(TAG, " %d", tiny_gps_.satellites.value()); + } + if (tiny_gps_.satellites.isUpdated()) { + ESP_LOGD(TAG, "HDOP:"); + ESP_LOGD(TAG, " %.2f", tiny_gps_.hdop.hdop()); + } + + for (auto *listener : this->listeners_) + listener->on_update(this->tiny_gps_); + } + } +} + } // namespace gps } // namespace esphome diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index 7d845d1bed..84a9248bc6 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -27,14 +27,7 @@ class GPS : public Component, public uart::UARTDevice { this->listeners_.push_back(listener); } float get_setup_priority() const override { return setup_priority::HARDWARE; } - void loop() override { - while (this->available() && !this->has_time_) { - if (this->tiny_gps_.encode(this->read())) { - for (auto *listener : this->listeners_) - listener->on_update(this->tiny_gps_); - } - } - } + void loop() override; TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; } protected: diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index c6aa8adc67..468ad09bac 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -6,5 +6,29 @@ namespace gps { static const char *TAG = "gps.time"; +void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { + if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid()) + return; + if (!tiny_gps.time.isUpdated() || !tiny_gps.date.isUpdated()) + return; + if (tiny_gps.date.year() < 2019) + return; + + time::ESPTime val{}; + val.year = tiny_gps.date.year(); + val.month = tiny_gps.date.month(); + val.day_of_month = tiny_gps.date.day(); + // Set these to valid value for recalc_timestamp_utc - it's not used for calculation + val.day_of_week = 1; + val.day_of_year = 1; + + val.hour = tiny_gps.time.hour(); + val.minute = tiny_gps.time.minute(); + val.second = tiny_gps.time.second(); + val.recalc_timestamp_utc(false); + this->synchronize_epoch_(val.timestamp); + this->has_time_ = true; +} + } // namespace gps } // namespace esphome diff --git a/esphome/components/gps/time/gps_time.h b/esphome/components/gps/time/gps_time.h index b09aee364f..f6462be3e0 100644 --- a/esphome/components/gps/time/gps_time.h +++ b/esphome/components/gps/time/gps_time.h @@ -18,20 +18,7 @@ class GPSTime : public time::RealTimeClock, public GPSListener { } protected: - void from_tiny_gps_(TinyGPSPlus &tiny_gps) { - if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid()) - return; - time::ESPTime val{}; - val.year = tiny_gps.date.year(); - val.month = tiny_gps.date.month(); - val.day_of_month = tiny_gps.date.day(); - val.hour = tiny_gps.time.hour(); - val.minute = tiny_gps.time.minute(); - val.second = tiny_gps.time.second(); - val.recalc_timestamp_utc(false); - this->synchronize_epoch_(val.timestamp); - this->has_time_ = true; - } + void from_tiny_gps_(TinyGPSPlus &tiny_gps); bool has_time_{false}; }; diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 81637039ca..4041c0c464 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -19,7 +19,7 @@ void HDC1080Component::setup() { 0b00000000 // reserved }; - if (this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) { + if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) { this->mark_failed(); return; } diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index b9321b51c6..4e5dc0f67f 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -8,6 +8,8 @@ namespace esphome { namespace hlw8012 { +enum HLW8012InitialMode { HLW8012_INITIAL_MODE_CURRENT = 0, HLW8012_INITIAL_MODE_VOLTAGE }; + class HLW8012Component : public PollingComponent { public: void setup() override; @@ -15,6 +17,9 @@ class HLW8012Component : public PollingComponent { float get_setup_priority() const override; void update() override; + void set_initial_mode(HLW8012InitialMode initial_mode) { + current_mode_ = initial_mode == HLW8012_INITIAL_MODE_CURRENT; + } void set_change_mode_every(uint32_t change_mode_every) { change_mode_every_ = change_mode_every; } void set_current_resistor(float current_resistor) { current_resistor_ = current_resistor; } void set_voltage_divider(float voltage_divider) { voltage_divider_ = voltage_divider; } diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 697c34f9d2..e1f02b8fd2 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_CHANGE_MODE_EVERY, CONF_CURRENT, \ +from esphome.const import CONF_CHANGE_MODE_EVERY, CONF_INITIAL_MODE, CONF_CURRENT, \ CONF_CURRENT_RESISTOR, CONF_ID, CONF_POWER, CONF_SEL_PIN, CONF_VOLTAGE, CONF_VOLTAGE_DIVIDER, \ ICON_FLASH, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT @@ -10,6 +10,11 @@ AUTO_LOAD = ['pulse_counter'] hlw8012_ns = cg.esphome_ns.namespace('hlw8012') HLW8012Component = hlw8012_ns.class_('HLW8012Component', cg.PollingComponent) +HLW8012InitialMode = hlw8012_ns.enum('HLW8012InitialMode') +INITIAL_MODES = { + CONF_CURRENT: HLW8012InitialMode.HLW8012_INITIAL_MODE_CURRENT, + CONF_VOLTAGE: HLW8012InitialMode.HLW8012_INITIAL_MODE_VOLTAGE, +} CONF_CF1_PIN = 'cf1_pin' CONF_CF_PIN = 'cf_pin' @@ -28,6 +33,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All(cv.uint32_t, cv.Range(min=1)), + cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of(*INITIAL_MODES, lower=True), }).extend(cv.polling_component_schema('60s')) @@ -54,3 +60,4 @@ def to_code(config): cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR])) cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER])) cg.add(var.set_change_mode_every(config[CONF_CHANGE_MODE_EVERY])) + cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]])) diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp index 61c73d272b..203f6d8a24 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp @@ -16,14 +16,16 @@ void HomeassistantBinarySensor::setup() { ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str()); break; case PARSE_ON: - ESP_LOGD(TAG, "'%s': Got state ON", this->entity_id_.c_str()); - this->publish_state(true); - break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Got state OFF", this->entity_id_.c_str()); - this->publish_state(false); + bool new_state = val == PARSE_ON; + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); + if (this->initial_) + this->publish_initial_state(new_state); + else + this->publish_state(new_state); break; } + this->initial_ = false; }); } void HomeassistantBinarySensor::dump_config() { diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h index c2c7ec4480..e468fd00eb 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h @@ -15,6 +15,7 @@ class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Com protected: std::string entity_id_; + bool initial_{true}; }; } // namespace homeassistant diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index b21fd4c0ce..e9d97690fb 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -22,19 +22,5 @@ void HomeassistantTime::setup() { HomeassistantTime *global_homeassistant_time = nullptr; -bool GetTimeResponse::decode_32bit(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: - // fixed32 epoch_seconds = 1; - if (global_homeassistant_time != nullptr) { - global_homeassistant_time->set_epoch_time(value); - } - return true; - default: - return false; - } -} -api::APIMessageType GetTimeResponse::message_type() const { return api::APIMessageType::GET_TIME_RESPONSE; } - } // namespace homeassistant } // namespace esphome diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h index 43937c6f13..8ab09d1185 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.h +++ b/esphome/components/homeassistant/time/homeassistant_time.h @@ -17,11 +17,5 @@ class HomeassistantTime : public time::RealTimeClock { extern HomeassistantTime *global_homeassistant_time; -class GetTimeResponse : public api::APIMessage { - public: - bool decode_32bit(uint32_t field_id, uint32_t value) override; - api::APIMessageType message_type() const override; -}; - } // namespace homeassistant } // namespace esphome diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 3fa9d2c37a..562bd26771 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -32,7 +32,7 @@ void I2CComponent::dump_config() { if (this->scan_) { ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); uint8_t found = 0; - for (uint8_t address = 8; address < 120; address++) { + for (uint8_t address = 1; address < 120; address++) { this->wire_->beginTransmission(address); uint8_t error = this->wire_->endTransmission(); @@ -135,6 +135,9 @@ bool I2CComponent::read_bytes(uint8_t address, uint8_t a_register, uint8_t *data delay(conversion); return this->raw_receive(address, data, len); } +bool I2CComponent::read_bytes_raw(uint8_t address, uint8_t *data, uint8_t len) { + return this->raw_receive(address, data, len); +} bool I2CComponent::read_bytes_16(uint8_t address, uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion) { if (!this->write_bytes(address, a_register, nullptr, 0)) @@ -156,6 +159,11 @@ bool I2CComponent::write_bytes(uint8_t address, uint8_t a_register, const uint8_ this->raw_write(address, data, len); return this->raw_end_transmission(address); } +bool I2CComponent::write_bytes_raw(uint8_t address, const uint8_t *data, uint8_t len) { + this->raw_begin_transmission(address); + this->raw_write(address, data, len); + return this->raw_end_transmission(address); +} bool I2CComponent::write_bytes_16(uint8_t address, uint8_t a_register, const uint16_t *data, uint8_t len) { this->raw_begin_transmission(address); this->raw_write(address, &a_register, 1); @@ -200,5 +208,30 @@ void I2CDevice::set_i2c_parent(I2CComponent *parent) { this->parent_ = parent; } uint8_t next_i2c_bus_num_ = 0; #endif +I2CRegister &I2CRegister::operator=(uint8_t value) { + this->parent_->write_byte(this->register_, value); + return *this; +} + +I2CRegister &I2CRegister::operator&=(uint8_t value) { + this->parent_->write_byte(this->register_, this->get() & value); + return *this; +} + +I2CRegister &I2CRegister::operator|=(uint8_t value) { + this->parent_->write_byte(this->register_, this->get() | value); + return *this; +} + +uint8_t I2CRegister::get() { + uint8_t value = 0x00; + this->parent_->read_byte(this->register_, &value); + return value; +} +I2CRegister &I2CRegister::operator=(const std::vector<uint8_t> &value) { + this->parent_->write_bytes(this->register_, value); + return *this; +} + } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index e41bd6c5e8..c4ed40e268 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -42,6 +42,7 @@ class I2CComponent : public Component { * @return If the operation was successful. */ bool read_bytes(uint8_t address, uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); + bool read_bytes_raw(uint8_t address, uint8_t *data, uint8_t len); /** Read len amount of 16-bit words (MSB first) from a register into data. * @@ -69,6 +70,7 @@ class I2CComponent : public Component { * @return If the operation was successful. */ bool write_bytes(uint8_t address, uint8_t a_register, const uint8_t *data, uint8_t len); + bool write_bytes_raw(uint8_t address, const uint8_t *data, uint8_t len); /** Write len amount of 16-bit words (MSB first) to the specified register for address. * @@ -132,6 +134,24 @@ class I2CComponent : public Component { extern uint8_t next_i2c_bus_num_; #endif +class I2CDevice; + +class I2CRegister { + public: + I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} + + I2CRegister &operator=(uint8_t value); + I2CRegister &operator=(const std::vector<uint8_t> &value); + I2CRegister &operator&=(uint8_t value); + I2CRegister &operator|=(uint8_t value); + + uint8_t get(); + + protected: + I2CDevice *parent_; + uint8_t register_; +}; + /** All components doing communication on the I2C bus should subclass I2CDevice. * * This class stores 1. the address of the i2c device and has a helper function to allow @@ -151,7 +171,8 @@ class I2CDevice { /// Manually set the parent i2c bus for this device. void set_i2c_parent(I2CComponent *parent); - protected: + I2CRegister reg(uint8_t a_register) { return {this, a_register}; } + /** Read len amount of bytes from a register into data. Optionally with a conversion time after * writing the register value to the bus. * @@ -161,15 +182,23 @@ class I2CDevice { * @param conversion The time in ms between writing the register value and reading out the value. * @return If the operation was successful. */ - bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); // NOLINT + bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); + bool read_bytes_raw(uint8_t *data, uint8_t len) { return this->parent_->read_bytes_raw(this->address_, data, len); } - template<size_t N> optional<std::array<uint8_t, N>> read_bytes(uint8_t a_register) { // NOLINT + template<size_t N> optional<std::array<uint8_t, N>> read_bytes(uint8_t a_register) { std::array<uint8_t, N> res; if (!this->read_bytes(a_register, res.data(), N)) { return {}; } return res; } + template<size_t N> optional<std::array<uint8_t, N>> read_bytes_raw() { + std::array<uint8_t, N> res; + if (!this->read_bytes_raw(res.data(), N)) { + return {}; + } + return res; + } /** Read len amount of 16-bit words (MSB first) from a register into data. * @@ -179,12 +208,12 @@ class I2CDevice { * @param conversion The time in ms between writing the register value and reading out the value. * @return If the operation was successful. */ - bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion = 0); // NOLINT + bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion = 0); /// Read a single byte from a register into the data variable. Return true if successful. - bool read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion = 0); // NOLINT + bool read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion = 0); - optional<uint8_t> read_byte(uint8_t a_register) { // NOLINT + optional<uint8_t> read_byte(uint8_t a_register) { uint8_t data; if (!this->read_byte(a_register, &data)) return {}; @@ -192,7 +221,7 @@ class I2CDevice { } /// Read a single 16-bit words (MSB first) from a register into the data variable. Return true if successful. - bool read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion = 0); // NOLINT + bool read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion = 0); /** Write len amount of 8-bit bytes to the specified register. * @@ -201,7 +230,10 @@ class I2CDevice { * @param len The amount of bytes to write to the bus. * @return If the operation was successful. */ - bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len); // NOLINT + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len); + bool write_bytes_raw(const uint8_t *data, uint8_t len) { + return this->parent_->write_bytes_raw(this->address_, data, len); + } /** Write a vector of data to a register. * @@ -209,13 +241,17 @@ class I2CDevice { * @param data The data to write. * @return If the operation was successful. */ - bool write_bytes(uint8_t a_register, const std::vector<uint8_t> &data) { // NOLINT + bool write_bytes(uint8_t a_register, const std::vector<uint8_t> &data) { return this->write_bytes(a_register, data.data(), data.size()); } + bool write_bytes_raw(const std::vector<uint8_t> &data) { return this->write_bytes_raw(data.data(), data.size()); } - template<size_t N> bool write_bytes(uint8_t a_register, const std::array<uint8_t, N> &data) { // NOLINT + template<size_t N> bool write_bytes(uint8_t a_register, const std::array<uint8_t, N> &data) { return this->write_bytes(a_register, data.data(), data.size()); } + template<size_t N> bool write_bytes_raw(const std::array<uint8_t, N> &data) { + return this->write_bytes_raw(data.data(), data.size()); + } /** Write len amount of 16-bit words (MSB first) to the specified register. * @@ -224,14 +260,15 @@ class I2CDevice { * @param len The amount of bytes to write to the bus. * @return If the operation was successful. */ - bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); // NOLINT + bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); /// Write a single byte of data into the specified register. Return true if successful. - bool write_byte(uint8_t a_register, uint8_t data); // NOLINT + bool write_byte(uint8_t a_register, uint8_t data); /// Write a single 16-bit word of data into the specified register. Return true if successful. - bool write_byte_16(uint8_t a_register, uint16_t data); // NOLINT + bool write_byte_16(uint8_t a_register, uint16_t data); + protected: uint8_t address_{0x00}; I2CComponent *parent_{nullptr}; }; diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index 9ddfd2ad0b..f9b5a43870 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -45,14 +45,14 @@ std::string IntegrationSensor::unit_of_measurement() { } void IntegrationSensor::process_sensor_value_(float value) { const uint32_t now = millis(); - const float old_value = this->last_value_; - const float new_value = value; + const double old_value = this->last_value_; + const double new_value = value; const uint32_t dt_ms = now - this->last_update_; - const float dt = dt_ms * this->get_time_factor_(); - float area = 0.0f; + const double dt = dt_ms * this->get_time_factor_(); + double area = 0.0f; switch (this->method_) { case INTEGRATION_METHOD_TRAPEZOID: - area = dt * (old_value + new_value) / 2.0f; + area = dt * (old_value + new_value) / 2.0; break; case INTEGRATION_METHOD_LEFT: area = dt * old_value; @@ -61,7 +61,9 @@ void IntegrationSensor::process_sensor_value_(float value) { area = dt * new_value; break; } - this->publish_and_save_(this->last_value_ + area); + this->last_value_ = new_value; + this->last_update_ = now; + this->publish_and_save_(this->result_ + area); } } // namespace integration diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index 6b1f4ccf1b..2fcec069b2 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -51,10 +51,11 @@ class IntegrationSensor : public sensor::Sensor, public Component { return 0.0f; } } - void publish_and_save_(float result) { + void publish_and_save_(double result) { this->result_ = result; this->publish_state(result); - this->rtc_.save(&result); + float result_f = result; + this->rtc_.save(&result_f); } std::string unit_of_measurement() override; std::string icon() override { return this->sensor_->get_icon(); } @@ -67,7 +68,7 @@ class IntegrationSensor : public sensor::Sensor, public Component { ESPPreferenceObject rtc_; uint32_t last_update_; - float result_{0.0f}; + double result_{0.0f}; float last_value_{0.0f}; }; diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py index 27f65f9336..bff194578c 100644 --- a/esphome/components/lcd_base/__init__.py +++ b/esphome/components/lcd_base/__init__.py @@ -1,12 +1,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import display -from esphome.const import CONF_DIMENSIONS, CONF_LAMBDA +from esphome.const import CONF_DIMENSIONS from esphome.core import coroutine lcd_base_ns = cg.esphome_ns.namespace('lcd_base') LCDDisplay = lcd_base_ns.class_('LCDDisplay', cg.PollingComponent) -LCDDisplayRef = LCDDisplay.operator('ref') def validate_lcd_dimensions(value): @@ -28,8 +27,3 @@ def setup_lcd_display(var, config): yield cg.register_component(var, config) yield display.register_display(var, config) cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1])) - - if CONF_LAMBDA in config: - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(LCDDisplayRef, 'it')], - return_type=cg.void) - cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index af6b8304eb..25ac143817 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -107,7 +107,7 @@ void LCDDisplay::update() { for (uint8_t i = 0; i < this->rows_ * this->columns_; i++) this->buffer_[i] = ' '; - this->writer_(*this); + this->call_writer(); this->display(); } void LCDDisplay::command_(uint8_t value) { this->send(value, false); } @@ -148,6 +148,11 @@ void LCDDisplay::printf(const char *format, ...) { if (ret > 0) this->print(0, 0, buffer); } +void LCDDisplay::clear() { + // clear display, also sets DDRAM address to 0 (home) + this->command_(LCD_DISPLAY_COMMAND_CLEAR_DISPLAY); + delay(2); +} #ifdef USE_TIME void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) { char buffer[64]; diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index 200600eb9c..ee150059c6 100644 --- a/esphome/components/lcd_base/lcd_display.h +++ b/esphome/components/lcd_base/lcd_display.h @@ -12,11 +12,8 @@ namespace lcd_base { class LCDDisplay; -using lcd_writer_t = std::function<void(LCDDisplay &)>; - class LCDDisplay : public PollingComponent { public: - void set_writer(lcd_writer_t &&writer) { this->writer_ = std::move(writer); } void set_dimensions(uint8_t columns, uint8_t rows) { this->columns_ = columns; this->rows_ = rows; @@ -26,6 +23,8 @@ class LCDDisplay : public PollingComponent { float get_setup_priority() const override; void update() override; void display(); + //// Clear LCD display + void clear(); /// Print the given text at the specified column and row. void print(uint8_t column, uint8_t row, const char *str); @@ -54,11 +53,11 @@ class LCDDisplay : public PollingComponent { virtual void send(uint8_t value, bool rs) = 0; void command_(uint8_t value); + virtual void call_writer() = 0; uint8_t columns_; uint8_t rows_; uint8_t *buffer_{nullptr}; - lcd_writer_t writer_; }; } // namespace lcd_base diff --git a/esphome/components/lcd_gpio/display.py b/esphome/components/lcd_gpio/display.py index 1f98955ece..91498d59c9 100644 --- a/esphome/components/lcd_gpio/display.py +++ b/esphome/components/lcd_gpio/display.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import lcd_base -from esphome.const import CONF_DATA_PINS, CONF_ENABLE_PIN, CONF_RS_PIN, CONF_RW_PIN, CONF_ID +from esphome.const import CONF_DATA_PINS, CONF_ENABLE_PIN, CONF_RS_PIN, CONF_RW_PIN, CONF_ID, \ + CONF_LAMBDA AUTO_LOAD = ['lcd_base'] @@ -42,3 +43,9 @@ def to_code(config): if CONF_RW_PIN in config: rw = yield cg.gpio_pin_expression(config[CONF_RW_PIN]) cg.add(var.set_rw_pin(rw)) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], + [(GPIOLCDDisplay.operator('ref'), 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.h b/esphome/components/lcd_gpio/gpio_lcd_display.h index ed3b0c1137..01f6f95d9a 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.h +++ b/esphome/components/lcd_gpio/gpio_lcd_display.h @@ -8,6 +8,7 @@ namespace lcd_gpio { class GPIOLCDDisplay : public lcd_base::LCDDisplay { public: + void set_writer(std::function<void(GPIOLCDDisplay &)> &&writer) { this->writer_ = std::move(writer); } void setup() override; void set_data_pins(GPIOPin *d0, GPIOPin *d1, GPIOPin *d2, GPIOPin *d3) { this->data_pins_[0] = d0; @@ -36,10 +37,13 @@ class GPIOLCDDisplay : public lcd_base::LCDDisplay { void write_n_bits(uint8_t value, uint8_t n) override; void send(uint8_t value, bool rs) override; + void call_writer() override { this->writer_(*this); } + GPIOPin *rs_pin_{nullptr}; GPIOPin *rw_pin_{nullptr}; GPIOPin *enable_pin_{nullptr}; GPIOPin *data_pins_[8]{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; + std::function<void(GPIOLCDDisplay &)> writer_; }; } // namespace lcd_gpio diff --git a/esphome/components/lcd_pcf8574/display.py b/esphome/components/lcd_pcf8574/display.py index 2bc04a283f..2bbb3a2f7b 100644 --- a/esphome/components/lcd_pcf8574/display.py +++ b/esphome/components/lcd_pcf8574/display.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import lcd_base, i2c -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_LAMBDA DEPENDENCIES = ['i2c'] AUTO_LOAD = ['lcd_base'] @@ -18,3 +18,9 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield lcd_base.setup_lcd_display(var, config) yield i2c.register_i2c_device(var, config) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], + [(PCF8574LCDDisplay.operator('ref'), 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.cpp b/esphome/components/lcd_pcf8574/pcf8574_display.cpp index 59491c7d5a..e3002da25d 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.cpp +++ b/esphome/components/lcd_pcf8574/pcf8574_display.cpp @@ -6,9 +6,13 @@ namespace lcd_pcf8574 { static const char *TAG = "lcd_pcf8574"; +static const uint8_t LCD_DISPLAY_BACKLIGHT_ON = 0x08; +static const uint8_t LCD_DISPLAY_BACKLIGHT_OFF = 0x00; + void PCF8574LCDDisplay::setup() { ESP_LOGCONFIG(TAG, "Setting up PCF8574 LCD Display..."); - if (!this->write_bytes(0x08, nullptr, 0)) { + this->backlight_value_ = LCD_DISPLAY_BACKLIGHT_ON; + if (!this->write_bytes(this->backlight_value_, nullptr, 0)) { this->mark_failed(); return; } @@ -29,7 +33,7 @@ void PCF8574LCDDisplay::write_n_bits(uint8_t value, uint8_t n) { // Ugly fix: in the super setup() with n == 4 value needs to be shifted left value <<= 4; } - uint8_t data = value | 0x08; // Enable backlight + uint8_t data = value | this->backlight_value_; // Set backlight state this->write_bytes(data, nullptr, 0); // Pulse ENABLE this->write_bytes(data | 0x04, nullptr, 0); @@ -41,6 +45,14 @@ void PCF8574LCDDisplay::send(uint8_t value, bool rs) { this->write_n_bits((value & 0xF0) | rs, 0); this->write_n_bits(((value << 4) & 0xF0) | rs, 0); } +void PCF8574LCDDisplay::backlight() { + this->backlight_value_ = LCD_DISPLAY_BACKLIGHT_ON; + this->write_bytes(this->backlight_value_, nullptr, 0); +} +void PCF8574LCDDisplay::no_backlight() { + this->backlight_value_ = LCD_DISPLAY_BACKLIGHT_OFF; + this->write_bytes(this->backlight_value_, nullptr, 0); +} } // namespace lcd_pcf8574 } // namespace esphome diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.h b/esphome/components/lcd_pcf8574/pcf8574_display.h index 133679c501..4db3afb9b0 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.h +++ b/esphome/components/lcd_pcf8574/pcf8574_display.h @@ -9,13 +9,22 @@ namespace lcd_pcf8574 { class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice { public: + void set_writer(std::function<void(PCF8574LCDDisplay &)> &&writer) { this->writer_ = std::move(writer); } void setup() override; void dump_config() override; + void backlight(); + void no_backlight(); protected: bool is_four_bit_mode() override { return true; } void write_n_bits(uint8_t value, uint8_t n) override; void send(uint8_t value, bool rs) override; + + void call_writer() override { this->writer_(*this); } + + // Stores the current state of the backlight. + uint8_t backlight_value_; + std::function<void(PCF8574LCDDisplay &)> writer_; }; } // namespace lcd_pcf8574 diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 64094478c0..2b1c181a62 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -11,10 +11,10 @@ namespace ledc { static const char *TAG = "ledc.output"; void LEDCOutput::write_state(float state) { - if (this->pin_->is_inverted()) { + if (this->pin_->is_inverted()) state = 1.0f - state; - } + this->duty_ = state; const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const float duty_rounded = roundf(state * max_duty); auto duty = static_cast<uint32_t>(duty_rounded); @@ -22,18 +22,45 @@ void LEDCOutput::write_state(float state) { } void LEDCOutput::setup() { - ledcSetup(this->channel_, this->frequency_, this->bit_depth_); - ledcAttachPin(this->pin_->get_pin(), this->channel_); - + this->apply_frequency(this->frequency_); this->turn_off(); + // Attach pin after setting default value + ledcAttachPin(this->pin_->get_pin(), this->channel_); } void LEDCOutput::dump_config() { ESP_LOGCONFIG(TAG, "LEDC Output:"); - LOG_PIN(" Pin", this->pin_); + LOG_PIN(" Pin ", this->pin_); ESP_LOGCONFIG(TAG, " LEDC Channel: %u", this->channel_); ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); - ESP_LOGCONFIG(TAG, " Bit Depth: %u", this->bit_depth_); +} + +float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return 80e6f / float(1 << bit_depth); } +float ledc_min_frequency_for_bit_depth(uint8_t bit_depth) { + const float max_div_num = ((1 << 20) - 1) / 256.0f; + return 80e6f / (max_div_num * float(1 << bit_depth)); +} +optional<uint8_t> ledc_bit_depth_for_frequency(float frequency) { + for (int i = 20; i >= 1; i--) { + const float min_frequency = ledc_min_frequency_for_bit_depth(i); + const float max_frequency = ledc_max_frequency_for_bit_depth(i); + if (min_frequency <= frequency && frequency <= max_frequency) + return i; + } + return {}; +} + +void LEDCOutput::apply_frequency(float frequency) { + auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); + if (!bit_depth_opt.has_value()) { + ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); + this->status_set_warning(); + } + this->bit_depth_ = bit_depth_opt.value_or(8); + this->frequency_ = frequency; + ledcSetup(this->channel_, frequency, this->bit_depth_); + // re-apply duty + this->write_state(this->duty_); } uint8_t next_ledc_channel = 0; diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index d1b9b099ee..3f56f502b0 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/esphal.h" +#include "esphome/core/automation.h" #include "esphome/components/output/float_output.h" #ifdef ARDUINO_ARCH_ESP32 @@ -16,11 +17,10 @@ class LEDCOutput : public output::FloatOutput, public Component { explicit LEDCOutput(GPIOPin *pin) : pin_(pin) { this->channel_ = next_ledc_channel++; } void set_channel(uint8_t channel) { this->channel_ = channel; } - void set_bit_depth(uint8_t bit_depth) { this->bit_depth_ = bit_depth; } void set_frequency(float frequency) { this->frequency_ = frequency; } + /// Dynamically change frequency at runtime + void apply_frequency(float frequency); - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) /// Setup LEDC. void setup() override; void dump_config() override; @@ -28,13 +28,28 @@ class LEDCOutput : public output::FloatOutput, public Component { float get_setup_priority() const override { return setup_priority::HARDWARE; } /// Override FloatOutput's write_state. - void write_state(float adjusted_value) override; + void write_state(float state) override; protected: GPIOPin *pin_; uint8_t channel_{}; uint8_t bit_depth_{}; float frequency_{}; + float duty_{0.0f}; +}; + +template<typename... Ts> class SetFrequencyAction : public Action<Ts...> { + public: + SetFrequencyAction(LEDCOutput *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, frequency); + + void play(Ts... x) { + float freq = this->frequency_.value(x...); + this->parent_->apply_frequency(freq); + } + + protected: + LEDCOutput *parent_; }; } // namespace ledc diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index c507465ff9..b608e9bbf7 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -1,6 +1,4 @@ -import math - -from esphome import pins +from esphome import pins, automation from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg @@ -15,53 +13,36 @@ def calc_max_frequency(bit_depth): def calc_min_frequency(bit_depth): - # LEDC_DIV_NUM_HSTIMER is 15-bit unsigned integer - # lower 8 bits represent fractional part - max_div_num = ((1 << 16) - 1) / 256.0 + max_div_num = ((2**20) - 1) / 256.0 return 80e6 / (max_div_num * (2**bit_depth)) -def validate_frequency_bit_depth(obj): - frequency = obj[CONF_FREQUENCY] - if CONF_BIT_DEPTH not in obj: - obj = obj.copy() - for bit_depth in range(15, 0, -1): - if calc_min_frequency(bit_depth) <= frequency <= calc_max_frequency(bit_depth): - obj[CONF_BIT_DEPTH] = bit_depth - break - else: - min_freq = min(calc_min_frequency(x) for x in range(1, 16)) - max_freq = max(calc_max_frequency(x) for x in range(1, 16)) - if frequency < min_freq: - raise cv.Invalid("This frequency setting is not possible, please choose a higher " - "frequency (at least {}Hz)".format(int(min_freq))) - if frequency > max_freq: - raise cv.Invalid("This frequency setting is not possible, please choose a lower " - "frequency (at most {}Hz)".format(int(max_freq))) - raise cv.Invalid("Invalid frequency!") - - bit_depth = obj[CONF_BIT_DEPTH] - min_freq = calc_min_frequency(bit_depth) - max_freq = calc_max_frequency(bit_depth) - if frequency > max_freq: - raise cv.Invalid('Maximum frequency for bit depth {} is {}Hz. Please decrease the ' - 'bit_depth.'.format(bit_depth, int(math.floor(max_freq)))) - if frequency < calc_min_frequency(bit_depth): - raise cv.Invalid('Minimum frequency for bit depth {} is {}Hz. Please increase the ' - 'bit_depth.'.format(bit_depth, int(math.ceil(min_freq)))) - return obj +def validate_frequency(value): + value = cv.frequency(value) + min_freq = calc_min_frequency(20) + max_freq = calc_max_frequency(1) + if value < min_freq: + raise cv.Invalid("This frequency setting is not possible, please choose a higher " + "frequency (at least {}Hz)".format(int(min_freq))) + if value > max_freq: + raise cv.Invalid("This frequency setting is not possible, please choose a lower " + "frequency (at most {}Hz)".format(int(max_freq))) + return value ledc_ns = cg.esphome_ns.namespace('ledc') LEDCOutput = ledc_ns.class_('LEDCOutput', output.FloatOutput, cg.Component) +SetFrequencyAction = ledc_ns.class_('SetFrequencyAction', automation.Action) -CONFIG_SCHEMA = cv.All(output.FLOAT_OUTPUT_SCHEMA.extend({ +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ cv.Required(CONF_ID): cv.declare_id(LEDCOutput), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_FREQUENCY, default='1kHz'): cv.frequency, - cv.Optional(CONF_BIT_DEPTH): cv.int_range(min=1, max=15), cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15), -}).extend(cv.COMPONENT_SCHEMA), validate_frequency_bit_depth) + + cv.Optional(CONF_BIT_DEPTH): cv.invalid("The bit_depth option has been removed in v1.14, the " + "best bit depth is now automatically calculated."), +}).extend(cv.COMPONENT_SCHEMA) def to_code(config): @@ -72,4 +53,15 @@ def to_code(config): if CONF_CHANNEL in config: cg.add(var.set_channel(config[CONF_CHANNEL])) cg.add(var.set_frequency(config[CONF_FREQUENCY])) - cg.add(var.set_bit_depth(config[CONF_BIT_DEPTH])) + + +@automation.register_action('output.ledc.set_frequency', SetFrequencyAction, cv.Schema({ + cv.Required(CONF_ID): cv.use_id(LEDCOutput), + cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), +})) +def ledc_set_frequency_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_FREQUENCY], args, float) + cg.add(var.set_frequency(template_)) + yield var diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index c93392418c..b5dc70a083 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -113,7 +113,7 @@ void ESPRangeView::fade_to_white(uint8_t amnt) { } void ESPRangeView::fade_to_black(uint8_t amnt) { for (auto c : *this) - c.fade_to_white(amnt); + c.fade_to_black(amnt); } void ESPRangeView::lighten(uint8_t delta) { for (auto c : *this) @@ -162,7 +162,6 @@ int32_t HOT interpret_index(int32_t index, int32_t size) { } void AddressableLight::call_setup() { - this->setup_internal_(); this->setup(); #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE @@ -180,5 +179,101 @@ void AddressableLight::call_setup() { #endif } +ESPColor esp_color_from_light_color_values(LightColorValues val) { + auto r = static_cast<uint8_t>(roundf(val.get_red() * 255.0f)); + auto g = static_cast<uint8_t>(roundf(val.get_green() * 255.0f)); + auto b = static_cast<uint8_t>(roundf(val.get_blue() * 255.0f)); + auto w = static_cast<uint8_t>(roundf(val.get_white() * val.get_state() * 255.0f)); + return ESPColor(r, g, b, w); +} + +void AddressableLight::write_state(LightState *state) { + auto val = state->current_values; + auto max_brightness = static_cast<uint8_t>(roundf(val.get_brightness() * val.get_state() * 255.0f)); + this->correction_.set_local_brightness(max_brightness); + + this->last_transition_progress_ = 0.0f; + this->accumulated_alpha_ = 0.0f; + + if (this->is_effect_active()) + return; + + // don't use LightState helper, gamma correction+brightness is handled by ESPColorView + + if (state->transformer_ == nullptr || !state->transformer_->is_transition()) { + // no transformer active or non-transition one + this->all() = esp_color_from_light_color_values(val); + } else { + // transition transformer active, activate specialized transition for addressable effects + // instead of using a unified transition for all LEDs, we use the current state each LED as the + // start. Warning: ugly + + // We can't use a direct lerp smoothing here though - that would require creating a copy of the original + // state of each LED at the start of the transition + // Instead, we "fake" the look of the LERP by using an exponential average over time and using + // dynamically-calculated alpha values to match the look of the + + float new_progress = state->transformer_->get_progress(); + float prev_smoothed = LightTransitionTransformer::smoothed_progress(last_transition_progress_); + float new_smoothed = LightTransitionTransformer::smoothed_progress(new_progress); + this->last_transition_progress_ = new_progress; + + auto end_values = state->transformer_->get_end_values(); + ESPColor target_color = esp_color_from_light_color_values(end_values); + + // our transition will handle brightness, disable brightness in correction. + this->correction_.set_local_brightness(255); + uint8_t orig_w = target_color.w; + target_color *= static_cast<uint8_t>(roundf(end_values.get_brightness() * end_values.get_state() * 255.0f)); + // w is not scaled by brightness + target_color.w = orig_w; + + float denom = (1.0f - new_smoothed); + float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom; + + // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length + // We solve this by accumulating the fractional part of the alpha over time. + float alpha255 = alpha * 255.0f; + float alpha255int = floorf(alpha255); + float alpha255remainder = alpha255 - alpha255int; + + this->accumulated_alpha_ += alpha255remainder; + float alpha_add = floorf(this->accumulated_alpha_); + this->accumulated_alpha_ -= alpha_add; + + alpha255 += alpha_add; + alpha255 = clamp(alpha255, 0.0f, 255.0f); + auto alpha8 = static_cast<uint8_t>(alpha255); + + if (alpha8 != 0) { + uint8_t inv_alpha8 = 255 - alpha8; + ESPColor add = target_color * alpha8; + + for (auto led : *this) + led = add + led.get() * inv_alpha8; + } + } + + this->schedule_show(); +} + +void ESPColorCorrection::calculate_gamma_table(float gamma) { + for (uint16_t i = 0; i < 256; i++) { + // corrected = val ^ gamma + auto corrected = static_cast<uint8_t>(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); + this->gamma_table_[i] = corrected; + } + if (gamma == 0.0f) { + for (uint16_t i = 0; i < 256; i++) + this->gamma_reverse_table_[i] = i; + return; + } + for (uint16_t i = 0; i < 256; i++) { + // val = corrected ^ (1/gamma) + auto uncorrected = static_cast<uint8_t>(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); + this->gamma_reverse_table_[i] = uncorrected; + } +} + } // namespace light } // namespace esphome diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 4383b4b245..a95d70f274 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -189,23 +189,7 @@ class ESPColorCorrection { ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} void set_max_brightness(const ESPColor &max_brightness) { this->max_brightness_ = max_brightness; } void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; } - void calculate_gamma_table(float gamma) { - for (uint16_t i = 0; i < 256; i++) { - // corrected = val ^ gamma - auto corrected = static_cast<uint8_t>(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); - this->gamma_table_[i] = corrected; - } - if (gamma == 0.0f) { - for (uint16_t i = 0; i < 256; i++) - this->gamma_reverse_table_[i] = i; - return; - } - for (uint16_t i = 0; i < 256; i++) { - // val = corrected ^ (1/gamma) - auto uncorrected = static_cast<uint8_t>(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); - this->gamma_reverse_table_[i] = uncorrected; - } - } + void calculate_gamma_table(float gamma); inline ESPColor color_correct(ESPColor color) const ALWAYS_INLINE { // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma return ESPColor(this->color_correct_red(color.red), this->color_correct_green(color.green), @@ -468,23 +452,7 @@ class AddressableLight : public LightOutput, public Component { } bool is_effect_active() const { return this->effect_active_; } void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; } - void write_state(LightState *state) override { - auto val = state->current_values; - auto max_brightness = static_cast<uint8_t>(roundf(val.get_brightness() * val.get_state() * 255.0f)); - this->correction_.set_local_brightness(max_brightness); - - if (this->is_effect_active()) - return; - - // don't use LightState helper, gamma correction+brightness is handled by ESPColorView - ESPColor color = ESPColor(uint8_t(roundf(val.get_red() * 255.0f)), uint8_t(roundf(val.get_green() * 255.0f)), - uint8_t(roundf(val.get_blue() * 255.0f)), - // white is not affected by brightness; so manually scale by state - uint8_t(roundf(val.get_white() * val.get_state() * 255.0f))); - - this->all() = color; - this->schedule_show(); - } + void write_state(LightState *state) override; void set_correction(float red, float green, float blue, float white = 1.0f) { this->correction_.set_max_brightness(ESPColor(uint8_t(roundf(red * 255.0f)), uint8_t(roundf(green * 255.0f)), uint8_t(roundf(blue * 255.0f)), uint8_t(roundf(white * 255.0f)))); @@ -524,6 +492,8 @@ class AddressableLight : public LightOutput, public Component { power_supply::PowerSupplyRequester power_; #endif LightState *state_parent_{nullptr}; + float last_transition_progress_{0.0f}; + float accumulated_alpha_{0.0f}; }; } // namespace light diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 1e0b540285..78ae41baad 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -50,19 +50,19 @@ class AddressableLightEffect : public LightEffect { class AddressableLambdaLightEffect : public AddressableLightEffect { public: - AddressableLambdaLightEffect(const std::string &name, const std::function<void(AddressableLight &)> &f, + AddressableLambdaLightEffect(const std::string &name, const std::function<void(AddressableLight &, ESPColor)> &f, uint32_t update_interval) : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {} void apply(AddressableLight &it, const ESPColor ¤t_color) override { const uint32_t now = millis(); if (now - this->last_run_ >= this->update_interval_) { this->last_run_ = now; - this->f_(it); + this->f_(it, current_color); } } protected: - std::function<void(AddressableLight &)> f_; + std::function<void(AddressableLight &, ESPColor)> f_; uint32_t update_interval_; uint32_t last_run_{0}; }; @@ -143,14 +143,19 @@ class AddressableScanEffect : public AddressableLightEffect { public: explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {} void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } + void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } void apply(AddressableLight &it, const ESPColor ¤t_color) override { it.all() = ESPColor::BLACK; - it[this->at_led_] = current_color; + + for (auto i = 0; i < this->scan_width_; i++) { + it[this->at_led_ + i] = current_color; + } + const uint32_t now = millis(); if (now - this->last_move_ > this->move_interval_) { if (direction_) { this->at_led_++; - if (this->at_led_ == it.size() - 1) + if (this->at_led_ == it.size() - this->scan_width_) this->direction_ = false; } else { this->at_led_--; @@ -163,6 +168,7 @@ class AddressableScanEffect : public AddressableLightEffect { protected: uint32_t move_interval_{}; + uint32_t scan_width_{1}; uint32_t last_move_{0}; int at_led_{0}; bool direction_{true}; @@ -312,11 +318,16 @@ class AddressableFlickerEffect : public AddressableLightEffect { const uint8_t inv_intensity = 255 - intensity; if (now - this->last_update_ < this->update_interval_) return; + this->last_update_ = now; fast_random_set_seed(random_uint32()); for (auto var : it) { const uint8_t flicker = fast_random_8() % intensity; - var = (var.get() * inv_intensity) + (current_color * flicker); + // scale down by random factor + var = var.get() * (255 - flicker); + + // slowly fade back to "real" value + var = (var.get() * inv_intensity) + (current_color * intensity); } } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 70c423af0a..2cd55ab6f6 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -122,6 +122,7 @@ template<typename... Ts> class AddressableSet : public Action<Ts...> { range.set_blue(this->blue_.value(x...)); if (this->white_.has_value()) range.set_white(this->white_.value(x...)); + out->schedule_show(); } protected: diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 55aa007f56..dcef60397d 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -102,6 +102,7 @@ class StrobeLightEffect : public LightEffect { if (!color.is_on()) { // Don't turn the light off, otherwise the light effect will be stopped call.set_brightness_if_supported(0.0f); + call.set_white_if_supported(0.0f); call.set_state(true); } call.set_publish(false); diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index a78165fb8a..c2250e7e0c 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -11,11 +11,12 @@ from .types import LambdaLightEffect, RandomLightEffect, StrobeLightEffect, \ FlickerLightEffect, AddressableRainbowLightEffect, AddressableColorWipeEffect, \ AddressableColorWipeEffectColor, AddressableScanEffect, AddressableTwinkleEffect, \ AddressableRandomTwinkleEffect, AddressableFireworksEffect, AddressableFlickerEffect, \ - AutomationLightEffect + AutomationLightEffect, ESPColor CONF_ADD_LED_INTERVAL = 'add_led_interval' CONF_REVERSE = 'reverse' CONF_MOVE_INTERVAL = 'move_interval' +CONF_SCAN_WIDTH = 'scan_width' CONF_TWINKLE_PROBABILITY = 'twinkle_probability' CONF_PROGRESS_INTERVAL = 'progress_interval' CONF_SPARK_PROBABILITY = 'spark_probability' @@ -128,7 +129,7 @@ def flicker_effect_to_code(config, effect_id): cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.positive_time_period_milliseconds, }) def addressable_lambda_effect_to_code(config, effect_id): - args = [(AddressableLightRef, 'it')] + args = [(AddressableLightRef, 'it'), (ESPColor, 'current_color')] lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) var = cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL]) @@ -179,10 +180,12 @@ def addressable_color_wipe_effect_to_code(config, effect_id): @register_effect('addressable_scan', AddressableScanEffect, "Scan", { cv.Optional(CONF_MOVE_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SCAN_WIDTH, default=1): cv.int_range(min=1), }) def addressable_scan_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_move_interval(config[CONF_MOVE_INTERVAL])) + cg.add(var.set_scan_width(config[CONF_SCAN_WIDTH])) yield var diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index cafced27fc..e96d64ad1f 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -24,9 +24,12 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length) { LightState::LightState(const std::string &name, LightOutput *output) : Nameable(name), output_(output) {} -void LightState::set_immediately_(const LightColorValues &target) { +void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { this->transformer_ = nullptr; - this->current_values = this->remote_values = target; + this->current_values = target; + if (set_remote_values) { + this->remote_values = target; + } this->next_write_ = true; } @@ -327,10 +330,10 @@ void LightCall::perform() { // Also set light color values when starting an effect // For example to turn off the light - this->parent_->set_immediately_(v); + this->parent_->set_immediately_(v, true); } else { // INSTANT CHANGE - this->parent_->set_immediately_(v); + this->parent_->set_immediately_(v, this->publish_); } if (this->publish_) { @@ -460,7 +463,8 @@ LightColorValues LightCall::validate_() { this->transition_length_.reset(); } - if (!this->has_transition_() && !this->has_flash_() && !this->has_effect_() && supports_transition) { + if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && + supports_transition) { // nothing specified and light supports transitions, set default transition length this->transition_length_ = this->parent_->default_transition_length_; } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index d67aa2c53d..07a0e3147b 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -277,6 +277,7 @@ class LightState : public Nameable, public Component { protected: friend LightOutput; friend LightCall; + friend class AddressableLight; uint32_t hash_base() override; @@ -291,7 +292,7 @@ class LightState : public Nameable, public Component { void start_flash_(const LightColorValues &target, uint32_t length); /// Internal method to set the color values to target immediately (with no transition). - void set_immediately_(const LightColorValues &target); + void set_immediately_(const LightColorValues &target, bool set_remote_values); /// Internal method to start a transformer. void set_transformer_(std::unique_ptr<LightTransformer> transformer); diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 91a406f425..222be7802c 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -17,7 +17,7 @@ class LightTransformer { LightTransformer() = delete; /// Whether this transformation is finished - virtual bool is_finished() { return this->get_progress_() >= 1.0f; } + virtual bool is_finished() { return this->get_progress() >= 1.0f; } /// This will be called to get the current values for output. virtual LightColorValues get_values() = 0; @@ -29,11 +29,11 @@ class LightTransformer { virtual LightColorValues get_end_values() { return this->get_target_values_(); } virtual bool publish_at_end() = 0; + virtual bool is_transition() = 0; + + float get_progress() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } protected: - /// Get the completion of this transformer, 0 to 1. - float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } - const LightColorValues &get_start_values_() const { return this->start_values_; } const LightColorValues &get_target_values_() const { return this->target_values_; } @@ -61,12 +61,14 @@ class LightTransitionTransformer : public LightTransformer { } LightColorValues get_values() override { - float x = this->get_progress_(); - float v = x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); + float v = LightTransitionTransformer::smoothed_progress(this->get_progress()); return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v); } bool publish_at_end() override { return false; } + bool is_transition() override { return true; } + + static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } }; class LightFlashTransformer : public LightTransformer { @@ -80,6 +82,7 @@ class LightFlashTransformer : public LightTransformer { LightColorValues get_end_values() override { return this->get_start_values_(); } bool publish_at_end() override { return true; } + bool is_transition() override { return false; } }; } // namespace light diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index 185105831d..fb88d021f2 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -9,6 +9,8 @@ AddressableLightState = light_ns.class_('LightState', LightState) LightOutput = light_ns.class_('LightOutput') AddressableLight = light_ns.class_('AddressableLight', cg.Component) AddressableLightRef = AddressableLight.operator('ref') + +ESPColor = light_ns.class_('ESPColor') LightColorValues = light_ns.class_('LightColorValues') # Actions diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 94b33ae18e..3e07334313 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -29,7 +29,7 @@ LOG_LEVEL_TO_ESP_LOG = { 'VERY_VERBOSE': cg.global_ns.ESP_LOGVV, } -LOG_LEVEL_SEVERITY = ['NONE', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'VERBOSE', 'VERY_VERBOSE'] +LOG_LEVEL_SEVERITY = ['NONE', 'ERROR', 'WARN', 'INFO', 'CONFIG', 'DEBUG', 'VERBOSE', 'VERY_VERBOSE'] UART_SELECTION_ESP32 = ['UART0', 'UART1', 'UART2'] @@ -123,6 +123,8 @@ def to_code(config): 'TLS_MEM', 'UPDATER', 'WIFI', + # Spams logs too much: + # 'MDNS_RESPONDER', } for comp in DEBUG_COMPONENTS: cg.add_build_flag("-DDEBUG_ESP_{}".format(comp)) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 78f09989e4..bc6951c9b9 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -10,38 +10,74 @@ namespace logger { static const char *TAG = "logger"; -int HOT Logger::log_vprintf_(int level, const char *tag, const char *format, va_list args) { // NOLINT - if (level > this->level_for(tag)) - return 0; +static const char *LOG_LEVEL_COLORS[] = { + "", // NONE + ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE +}; +static const char *LOG_LEVEL_LETTERS[] = { + "", // NONE + "E", // ERROR + "W", // WARNING + "I", // INFO + "C", // CONFIG + "D", // DEBUG + "V", // VERBOSE + "VV", // VERY_VERBOSE +}; - int ret = vsnprintf(this->tx_buffer_.data(), this->tx_buffer_.capacity(), format, args); - this->log_message_(level, tag, this->tx_buffer_.data(), ret); - return ret; +void Logger::write_header_(int level, const char *tag, int line) { + if (level < 0) + level = 0; + if (level > 7) + level = 7; + + const char *color = LOG_LEVEL_COLORS[level]; + const char *letter = LOG_LEVEL_LETTERS[level]; + this->printf_to_buffer_("%s[%s][%s:%03u]: ", color, letter, tag, line); +} + +void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT + if (level > this->level_for(tag)) + return; + + this->reset_buffer_(); + this->write_header_(level, tag, line); + this->vprintf_to_buffer_(format, args); + this->write_footer_(); + this->log_message_(level, tag); } #ifdef USE_STORE_LOG_STR_IN_FLASH -int Logger::log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args) { // NOLINT +void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, + va_list args) { // NOLINT if (level > this->level_for(tag)) - return 0; + return; + this->reset_buffer_(); // copy format string const char *format_pgm_p = (PGM_P) format; size_t len = 0; - char *write = this->tx_buffer_.data(); char ch = '.'; - while (len < this->tx_buffer_.capacity() && ch != '\0') { - *write++ = ch = pgm_read_byte(format_pgm_p++); - len++; + while (!this->is_buffer_full_() && ch != '\0') { + this->tx_buffer_[this->tx_buffer_at_++] = ch = pgm_read_byte(format_pgm_p++); } - if (len == this->tx_buffer_.capacity()) - return -1; + // Buffer full form copying format + if (this->is_buffer_full_()) + return; + + // length of format string, includes null terminator + uint32_t offset = this->tx_buffer_at_; // now apply vsnprintf - size_t offset = len + 1; - size_t remaining = this->tx_buffer_.capacity() - offset; - char *msg = this->tx_buffer_.data() + offset; - int ret = vsnprintf(msg, remaining, this->tx_buffer_.data(), args); - this->log_message_(level, tag, msg, ret); - return ret; + this->write_header_(level, tag, line); + this->vprintf_to_buffer_(this->tx_buffer_, args); + this->write_footer_(); + this->log_message_(level, tag, offset); } #endif @@ -54,22 +90,26 @@ int HOT Logger::level_for(const char *tag) { return it.level; } } - return this->global_log_level_; + return ESPHOME_LOG_LEVEL; } -void HOT Logger::log_message_(int level, const char *tag, char *msg, int ret) { - if (ret <= 0) - return; +void HOT Logger::log_message_(int level, const char *tag, int offset) { // remove trailing newline - if (msg[ret - 1] == '\n') { - msg[ret - 1] = '\0'; + if (this->tx_buffer_[this->tx_buffer_at_ - 1] == '\n') { + this->tx_buffer_at_--; } + // make sure null terminator is present + this->set_null_terminator_(); + + const char *msg = this->tx_buffer_ + offset; if (this->baud_rate_ > 0) this->hw_serial_->println(msg); this->log_callback_.call(level, tag, msg); } -Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart) : baud_rate_(baud_rate), uart_(uart) { - this->set_tx_buffer_size(tx_buffer_size); +Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart) + : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size), uart_(uart) { + // add 1 to buffer size for null terminator + this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; } void Logger::pre_setup() { @@ -96,7 +136,7 @@ void Logger::pre_setup() { if (this->uart_ == UART_SELECTION_UART0_SWAP) { this->hw_serial_->swap(); } - this->hw_serial_->setDebugOutput(this->global_log_level_ >= ESPHOME_LOG_LEVEL_VERBOSE); + this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); #endif } #ifdef ARDUINO_ARCH_ESP8266 @@ -108,7 +148,7 @@ void Logger::pre_setup() { global_logger = this; #ifdef ARDUINO_ARCH_ESP32 esp_log_set_vprintf(esp_idf_log_vprintf_); - if (this->global_log_level_ >= ESPHOME_LOG_LEVEL_VERBOSE) { + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { esp_log_level_set("*", ESP_LOG_VERBOSE); } #endif @@ -116,17 +156,15 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } -void Logger::set_global_log_level(int log_level) { this->global_log_level_ = log_level; } void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_.push_back(LogLevelOverride{tag, log_level}); } -void Logger::set_tx_buffer_size(size_t tx_buffer_size) { this->tx_buffer_.reserve(tx_buffer_size); } UARTSelection Logger::get_uart() const { return this->uart_; } void Logger::add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback) { this->log_callback_.add(std::move(callback)); } float Logger::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } -const char *LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; +const char *LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; #ifdef ARDUINO_ARCH_ESP32 const char *UART_SELECTIONS[] = {"UART0", "UART1", "UART2"}; #endif @@ -135,13 +173,14 @@ const char *UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; #endif void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:"); - ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[this->global_log_level_]); + ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); ESP_LOGCONFIG(TAG, " Log Baud Rate: %u", this->baud_rate_); ESP_LOGCONFIG(TAG, " Hardware UART: %s", UART_SELECTIONS[this->uart_]); for (auto &it : this->log_levels_) { ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.tag.c_str(), LOG_LEVELS[it.level]); } } +void Logger::write_footer_() { this->write_to_buffer_(ESPHOME_LOG_RESET_COLOR, strlen(ESPHOME_LOG_RESET_COLOR)); } Logger *global_logger = nullptr; diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 6f06c63595..b8a252c7bd 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -30,17 +30,11 @@ class Logger : public Component { /// Manually set the baud rate for serial, set to 0 to disable. void set_baud_rate(uint32_t baud_rate); - - /// Set the buffer size that's used for constructing log messages. Log messages longer than this will be truncated. - void set_tx_buffer_size(size_t tx_buffer_size); + uint32_t get_baud_rate() const { return baud_rate_; } /// Get the UART used by the logger. UARTSelection get_uart() const; - /// Set the global log level. Note: Use the ESPHOME_LOG_LEVEL define to also remove the logs from the build. - void set_global_log_level(int log_level); - int get_global_log_level() const { return this->global_log_level_; } - /// Set the log level of the specified tag. void set_log_level(const std::string &tag, int log_level); @@ -57,17 +51,58 @@ class Logger : public Component { float get_setup_priority() const override; - int log_vprintf_(int level, const char *tag, const char *format, va_list args); // NOLINT + void log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH - int log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args); // NOLINT + void log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); // NOLINT #endif protected: - void log_message_(int level, const char *tag, char *msg, int ret); + void write_header_(int level, const char *tag, int line); + void write_footer_(); + void log_message_(int level, const char *tag, int offset = 0); + + inline bool is_buffer_full_() const { return this->tx_buffer_at_ >= this->tx_buffer_size_; } + inline int buffer_remaining_capacity_() const { return this->tx_buffer_size_ - this->tx_buffer_at_; } + inline void reset_buffer_() { this->tx_buffer_at_ = 0; } + inline void set_null_terminator_() { + // does not increment buffer_at + this->tx_buffer_[this->tx_buffer_at_] = '\0'; + } + inline void write_to_buffer_(char value) { + if (!this->is_buffer_full_()) + this->tx_buffer_[this->tx_buffer_at_++] = value; + } + inline void write_to_buffer_(const char *value, int length) { + for (int i = 0; i < length && !this->is_buffer_full_(); i++) { + this->tx_buffer_[this->tx_buffer_at_++] = value[i]; + } + } + inline void vprintf_to_buffer_(const char *format, va_list args) { + if (this->is_buffer_full_()) + return; + int remaining = this->buffer_remaining_capacity_(); + int ret = vsnprintf(this->tx_buffer_ + this->tx_buffer_at_, remaining, format, args); + if (ret < 0) { + // Encoding error, do not increment buffer_at + return; + } + if (ret >= remaining) { + // output was too long, truncated + ret = remaining; + } + this->tx_buffer_at_ += ret; + } + inline void printf_to_buffer_(const char *format, ...) { + va_list arg; + va_start(arg, format); + this->vprintf_to_buffer_(format, arg); + va_end(arg); + } uint32_t baud_rate_; - std::vector<char> tx_buffer_; - int global_log_level_{ESPHOME_LOG_LEVEL}; + char *tx_buffer_{nullptr}; + int tx_buffer_at_{0}; + int tx_buffer_size_{0}; UARTSelection uart_{UART_SELECTION_UART0}; HardwareSerial *hw_serial_{nullptr}; struct LogLevelOverride { diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 18a00b10d7..0462ed4342 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -82,7 +82,5 @@ void MAX31855Sensor::read_data_() { this->status_clear_warning(); } -bool MAX31855Sensor::is_device_msb_first() { return true; } - } // namespace max31855 } // namespace esphome diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h index f9cdf335f1..1d0fc79ac0 100644 --- a/esphome/components/max31855/max31855.h +++ b/esphome/components/max31855/max31855.h @@ -7,7 +7,10 @@ namespace esphome { namespace max31855 { -class MAX31855Sensor : public sensor::Sensor, public PollingComponent, public spi::SPIDevice { +class MAX31855Sensor : public sensor::Sensor, + public PollingComponent, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, + spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_4MHZ> { public: void setup() override; void dump_config() override; @@ -16,8 +19,6 @@ class MAX31855Sensor : public sensor::Sensor, public PollingComponent, public sp void update() override; protected: - bool is_device_msb_first() override; - void read_data_(); }; diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index 8ea7feb963..53442b9cb1 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -48,7 +48,5 @@ void MAX6675Sensor::read_data_() { this->status_clear_warning(); } -bool MAX6675Sensor::is_device_msb_first() { return true; } - } // namespace max6675 } // namespace esphome diff --git a/esphome/components/max6675/max6675.h b/esphome/components/max6675/max6675.h index 48f51fbe11..09bd9df3b8 100644 --- a/esphome/components/max6675/max6675.h +++ b/esphome/components/max6675/max6675.h @@ -7,7 +7,10 @@ namespace esphome { namespace max6675 { -class MAX6675Sensor : public sensor::Sensor, public PollingComponent, public spi::SPIDevice { +class MAX6675Sensor : public sensor::Sensor, + public PollingComponent, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, + spi::DATA_RATE_1KHZ> { public: void setup() override; void dump_config() override; @@ -16,8 +19,6 @@ class MAX6675Sensor : public sensor::Sensor, public PollingComponent, public spi void update() override; protected: - bool is_device_msb_first() override; - void read_data_(); }; diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index bc3c3ae0c9..db43ff19f6 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -155,7 +155,6 @@ void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) { this->send_byte_(a_register, data); this->disable(); } -bool MAX7219Component::is_device_msb_first() { return true; } void MAX7219Component::update() { for (uint8_t i = 0; i < this->num_chips_ * 8; i++) this->buffer_[i] = 0; diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index e2379fa69b..1920268ba4 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -16,7 +16,9 @@ class MAX7219Component; using max7219_writer_t = std::function<void(MAX7219Component &)>; -class MAX7219Component : public PollingComponent, public spi::SPIDevice { +class MAX7219Component : public PollingComponent, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, + spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> { public: void set_writer(max7219_writer_t &&writer); @@ -54,7 +56,6 @@ class MAX7219Component : public PollingComponent, public spi::SPIDevice { protected: void send_byte_(uint8_t a_register, uint8_t data); void send_to_all_(uint8_t a_register, uint8_t data); - bool is_device_msb_first() override; uint8_t intensity_{15}; /// Intensity of the display from 0 to 15 (most) uint8_t num_chips_{1}; diff --git a/esphome/components/mcp23008/__init__.py b/esphome/components/mcp23008/__init__.py new file mode 100644 index 0000000000..4241b6ba48 --- /dev/null +++ b/esphome/components/mcp23008/__init__.py @@ -0,0 +1,51 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED + +DEPENDENCIES = ['i2c'] +MULTI_CONF = True + +mcp23008_ns = cg.esphome_ns.namespace('mcp23008') +MCP23008GPIOMode = mcp23008_ns.enum('MCP23008GPIOMode') +MCP23008_GPIO_MODES = { + 'INPUT': MCP23008GPIOMode.MCP23008_INPUT, + 'INPUT_PULLUP': MCP23008GPIOMode.MCP23008_INPUT_PULLUP, + 'OUTPUT': MCP23008GPIOMode.MCP23008_OUTPUT, +} + +MCP23008 = mcp23008_ns.class_('MCP23008', cg.Component, i2c.I2CDevice) +MCP23008GPIOPin = mcp23008_ns.class_('MCP23008GPIOPin', cg.GPIOPin) + +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(MCP23008), +}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + +CONF_MCP23008 = 'mcp23008' +MCP23008_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_MCP23008): cv.use_id(MCP23008), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(MCP23008_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +MCP23008_INPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_MCP23008): cv.use_id(MCP23008), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="INPUT"): cv.enum(MCP23008_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23008, + (MCP23008_OUTPUT_PIN_SCHEMA, MCP23008_INPUT_PIN_SCHEMA)) +def mcp23008_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_MCP23008]) + yield MCP23008GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp new file mode 100644 index 0000000000..bf5bb55f2e --- /dev/null +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -0,0 +1,91 @@ +#include "mcp23008.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23008 { + +static const char *TAG = "mcp23008"; + +void MCP23008::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP23008..."); + uint8_t iocon; + if (!this->read_reg_(MCP23008_IOCON, &iocon)) { + this->mark_failed(); + return; + } + + // all pins input + this->write_reg_(MCP23008_IODIR, 0xFF); +} +bool MCP23008::digital_read(uint8_t pin) { + uint8_t bit = pin % 8; + uint8_t reg_addr = MCP23008_GPIO; + uint8_t value = 0; + this->read_reg_(reg_addr, &value); + return value & (1 << bit); +} +void MCP23008::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = MCP23008_OLAT; + this->update_reg_(pin, value, reg_addr); +} +void MCP23008::pin_mode(uint8_t pin, uint8_t mode) { + uint8_t iodir = MCP23008_IODIR; + uint8_t gppu = MCP23008_GPPU; + switch (mode) { + case MCP23008_INPUT: + this->update_reg_(pin, true, iodir); + break; + case MCP23008_INPUT_PULLUP: + this->update_reg_(pin, true, iodir); + this->update_reg_(pin, true, gppu); + break; + case MCP23008_OUTPUT: + this->update_reg_(pin, false, iodir); + break; + default: + break; + } +} +float MCP23008::get_setup_priority() const { return setup_priority::HARDWARE; } +bool MCP23008::read_reg_(uint8_t reg, uint8_t *value) { + if (this->is_failed()) + return false; + + return this->read_byte(reg, value); +} +bool MCP23008::write_reg_(uint8_t reg, uint8_t value) { + if (this->is_failed()) + return false; + + return this->write_byte(reg, value); +} +void MCP23008::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == MCP23008_OLAT) { + reg_value = this->olat_; + } else { + this->read_reg_(reg_addr, ®_value); + } + + if (pin_value) + reg_value |= 1 << bit; + else + reg_value &= ~(1 << bit); + + this->write_reg_(reg_addr, reg_value); + + if (reg_addr == MCP23008_OLAT) { + this->olat_ = reg_value; + } +} + +MCP23008GPIOPin::MCP23008GPIOPin(MCP23008 *parent, uint8_t pin, uint8_t mode, bool inverted) + : GPIOPin(pin, mode, inverted), parent_(parent) {} +void MCP23008GPIOPin::setup() { this->pin_mode(this->mode_); } +void MCP23008GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } +bool MCP23008GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void MCP23008GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } + +} // namespace mcp23008 +} // namespace esphome diff --git a/esphome/components/mcp23008/mcp23008.h b/esphome/components/mcp23008/mcp23008.h new file mode 100644 index 0000000000..b4e5d75fd4 --- /dev/null +++ b/esphome/components/mcp23008/mcp23008.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp23008 { + +/// Modes for MCP23008 pins +enum MCP23008GPIOMode : uint8_t { + MCP23008_INPUT = INPUT, // 0x00 + MCP23008_INPUT_PULLUP = INPUT_PULLUP, // 0x02 + MCP23008_OUTPUT = OUTPUT // 0x01 +}; + +enum MCP23008GPIORegisters { + // A side + MCP23008_IODIR = 0x00, + MCP23008_IPOL = 0x01, + MCP23008_GPINTEN = 0x02, + MCP23008_DEFVAL = 0x03, + MCP23008_INTCON = 0x04, + MCP23008_IOCON = 0x05, + MCP23008_GPPU = 0x06, + MCP23008_INTF = 0x07, + MCP23008_INTCAP = 0x08, + MCP23008_GPIO = 0x09, + MCP23008_OLAT = 0x0A, +}; + +class MCP23008 : public Component, public i2c::I2CDevice { + public: + MCP23008() = default; + + void setup() override; + + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, uint8_t mode); + + float get_setup_priority() const override; + + protected: + // read a given register + bool read_reg_(uint8_t reg, uint8_t *value); + // write a value to a given register + bool write_reg_(uint8_t reg, uint8_t value); + // update registers with given pin value. + void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a); + + uint8_t olat_{0x00}; +}; + +class MCP23008GPIOPin : public GPIOPin { + public: + MCP23008GPIOPin(MCP23008 *parent, uint8_t pin, uint8_t mode, bool inverted = false); + + void setup() override; + void pin_mode(uint8_t mode) override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + MCP23008 *parent_; +}; + +} // namespace mcp23008 +} // namespace esphome diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 687c816179..9653aa680d 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -47,7 +47,7 @@ void MCP23017::pin_mode(uint8_t pin, uint8_t mode) { break; } } -float MCP23017::get_setup_priority() const { return setup_priority::HARDWARE; } +float MCP23017::get_setup_priority() const { return setup_priority::IO; } bool MCP23017::read_reg_(uint8_t reg, uint8_t *value) { if (this->is_failed()) return false; diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index 8f46e288b6..8e28d04dea 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -8,6 +8,9 @@ static const char *TAG = "mhz19"; static const uint8_t MHZ19_REQUEST_LENGTH = 8; static const uint8_t MHZ19_RESPONSE_LENGTH = 9; static const uint8_t MHZ19_COMMAND_GET_PPM[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t MHZ19_COMMAND_ABC_ENABLE[] = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t MHZ19_COMMAND_ABC_DISABLE[] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t MHZ19_COMMAND_CALIBRATE_ZERO[] = {0xFF, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00}; uint8_t mhz19_checksum(const uint8_t *command) { uint8_t sum = 0; @@ -17,6 +20,14 @@ uint8_t mhz19_checksum(const uint8_t *command) { return 0xFF - sum + 0x01; } +void MHZ19Component::setup() { + if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) { + this->abc_enable(); + } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) { + this->abc_disable(); + } +} + void MHZ19Component::update() { uint8_t response[MHZ19_RESPONSE_LENGTH]; if (!this->mhz19_write_command_(MHZ19_COMMAND_GET_PPM, response)) { @@ -50,23 +61,46 @@ void MHZ19Component::update() { this->temperature_sensor_->publish_state(temp); } +void MHZ19Component::calibrate_zero() { + ESP_LOGD(TAG, "MHZ19 Calibrating zero point"); + this->mhz19_write_command_(MHZ19_COMMAND_CALIBRATE_ZERO, nullptr); +} + +void MHZ19Component::abc_enable() { + ESP_LOGD(TAG, "MHZ19 Enabling automatic baseline calibration"); + this->mhz19_write_command_(MHZ19_COMMAND_ABC_ENABLE, nullptr); +} + +void MHZ19Component::abc_disable() { + ESP_LOGD(TAG, "MHZ19 Disabling automatic baseline calibration"); + this->mhz19_write_command_(MHZ19_COMMAND_ABC_DISABLE, nullptr); +} + bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *response) { - this->flush(); + // Empty RX Buffer + while (this->available()) + this->read(); this->write_array(command, MHZ19_REQUEST_LENGTH); this->write_byte(mhz19_checksum(command)); + this->flush(); if (response == nullptr) return true; - bool ret = this->read_array(response, MHZ19_RESPONSE_LENGTH); - this->flush(); - return ret; + return this->read_array(response, MHZ19_RESPONSE_LENGTH); } float MHZ19Component::get_setup_priority() const { return setup_priority::DATA; } void MHZ19Component::dump_config() { ESP_LOGCONFIG(TAG, "MH-Z19:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + this->check_uart_settings(9600); + + if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) { + ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot"); + } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) { + ESP_LOGCONFIG(TAG, " Automatic baseline calibration disabled on boot"); + } } } // namespace mhz19 diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index 3604628afc..2201fc87f0 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -1,27 +1,64 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/automation.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" namespace esphome { namespace mhz19 { +enum MHZ19ABCLogic { MHZ19_ABC_NONE = 0, MHZ19_ABC_ENABLED, MHZ19_ABC_DISABLED }; + class MHZ19Component : public PollingComponent, public uart::UARTDevice { public: float get_setup_priority() const override; + void setup() override; void update() override; void dump_config() override; + void calibrate_zero(); + void abc_enable(); + void abc_disable(); + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + void set_abc_enabled(bool abc_enabled) { abc_boot_logic_ = abc_enabled ? MHZ19_ABC_ENABLED : MHZ19_ABC_DISABLED; } protected: bool mhz19_write_command_(const uint8_t *command, uint8_t *response); sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *co2_sensor_{nullptr}; + MHZ19ABCLogic abc_boot_logic_{MHZ19_ABC_NONE}; +}; + +template<typename... Ts> class MHZ19CalibrateZeroAction : public Action<Ts...> { + public: + MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->calibrate_zero(); } + + protected: + MHZ19Component *mhz19_; +}; + +template<typename... Ts> class MHZ19ABCEnableAction : public Action<Ts...> { + public: + MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_enable(); } + + protected: + MHZ19Component *mhz19_; +}; + +template<typename... Ts> class MHZ19ABCDisableAction : public Action<Ts...> { + public: + MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_disable(); } + + protected: + MHZ19Component *mhz19_; }; } // namespace mhz19 diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 368426e6f7..bdcecf12cb 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -1,18 +1,26 @@ 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 sensor, uart from esphome.const import CONF_CO2, CONF_ID, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, ICON_THERMOMETER DEPENDENCIES = ['uart'] +CONF_AUTOMATIC_BASELINE_CALIBRATION = 'automatic_baseline_calibration' + mhz19_ns = cg.esphome_ns.namespace('mhz19') MHZ19Component = mhz19_ns.class_('MHZ19Component', cg.PollingComponent, uart.UARTDevice) +MHZ19CalibrateZeroAction = mhz19_ns.class_('MHZ19CalibrateZeroAction', automation.Action) +MHZ19ABCEnableAction = mhz19_ns.class_('MHZ19ABCEnableAction', automation.Action) +MHZ19ABCDisableAction = mhz19_ns.class_('MHZ19ABCDisableAction', automation.Action) CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(MHZ19Component), cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 0), + cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) @@ -28,3 +36,22 @@ def to_code(config): if CONF_TEMPERATURE in config: sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) + + if CONF_AUTOMATIC_BASELINE_CALIBRATION in config: + cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION])) + + +CALIBRATION_ACTION_SCHEMA = maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(MHZ19Component), +}) + + +@automation.register_action('mhz19.calibrate_zero', MHZ19CalibrateZeroAction, + CALIBRATION_ACTION_SCHEMA) +@automation.register_action('mhz19.abc_enable', MHZ19ABCEnableAction, + CALIBRATION_ACTION_SCHEMA) +@automation.register_action('mhz19.abc_disable', MHZ19ABCDisableAction, + CALIBRATION_ACTION_SCHEMA) +def mhz19_calibration_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py new file mode 100644 index 0000000000..cada835905 --- /dev/null +++ b/esphome/components/modbus/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID, CONF_ADDRESS +from esphome.core import coroutine + +DEPENDENCIES = ['uart'] + +modbus_ns = cg.esphome_ns.namespace('modbus') +Modbus = modbus_ns.class_('Modbus', cg.Component, uart.UARTDevice) +ModbusDevice = modbus_ns.class_('ModbusDevice') +MULTI_CONF = True + +CONF_MODBUS_ID = 'modbus_id' +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Modbus), +}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + cg.add_global(modbus_ns.using) + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + yield uart.register_uart_device(var, config) + + +def modbus_device_schema(default_address): + schema = { + cv.GenerateID(CONF_MODBUS_ID): cv.use_id(Modbus), + } + if default_address is None: + schema[cv.Required(CONF_ADDRESS)] = cv.hex_uint8_t + else: + schema[cv.Optional(CONF_ADDRESS, default=default_address)] = cv.hex_uint8_t + return cv.Schema(schema) + + +@coroutine +def register_modbus_device(var, config): + parent = yield cg.get_variable(config[CONF_MODBUS_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_address(config[CONF_ADDRESS])) + cg.add(parent.register_device(var)) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp new file mode 100644 index 0000000000..74d0c40986 --- /dev/null +++ b/esphome/components/modbus/modbus.cpp @@ -0,0 +1,119 @@ +#include "modbus.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus { + +static const char *TAG = "modbus"; + +void Modbus::loop() { + const uint32_t now = millis(); + if (now - this->last_modbus_byte_ > 50) { + this->rx_buffer_.clear(); + this->last_modbus_byte_ = now; + } + + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + if (this->parse_modbus_byte_(byte)) { + this->last_modbus_byte_ = now; + } else { + this->rx_buffer_.clear(); + } + } +} + +uint16_t crc16(const uint8_t *data, uint8_t len) { + uint16_t crc = 0xFFFF; + while (len--) { + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) { + if ((crc & 0x01) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; +} + +bool Modbus::parse_modbus_byte_(uint8_t byte) { + size_t at = this->rx_buffer_.size(); + this->rx_buffer_.push_back(byte); + const uint8_t *raw = &this->rx_buffer_[0]; + + // Byte 0: modbus address (match all) + if (at == 0) + return true; + uint8_t address = raw[0]; + + // Byte 1: Function (msb indicates error) + if (at == 1) + return (byte & 0x80) != 0x80; + + // Byte 2: Size (with modbus rtu function code 4/3) + // See also https://en.wikipedia.org/wiki/Modbus + if (at == 2) + return true; + + uint8_t data_len = raw[2]; + // Byte 3..3+data_len-1: Data + if (at < 3 + data_len) + return true; + + // Byte 3+data_len: CRC_LO (over all bytes) + if (at == 3 + data_len) + return true; + // Byte 3+len+1: CRC_HI (over all bytes) + uint16_t computed_crc = crc16(raw, 3 + data_len); + uint16_t remote_crc = uint16_t(raw[3 + data_len]) | (uint16_t(raw[3 + data_len + 1]) << 8); + if (computed_crc != remote_crc) { + ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); + return false; + } + + std::vector<uint8_t> data(this->rx_buffer_.begin() + 3, this->rx_buffer_.begin() + 3 + data_len); + + bool found = false; + for (auto *device : this->devices_) { + if (device->address_ == address) { + device->on_modbus_data(data); + found = true; + } + } + if (!found) { + ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X!", address); + } + + // return false to reset buffer + return false; +} + +void Modbus::dump_config() { + ESP_LOGCONFIG(TAG, "Modbus:"); + this->check_uart_settings(9600, 2); +} +float Modbus::get_setup_priority() const { + // After UART bus + return setup_priority::BUS - 1.0f; +} +void Modbus::send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count) { + uint8_t frame[8]; + frame[0] = address; + frame[1] = function; + frame[2] = start_address >> 8; + frame[3] = start_address >> 0; + frame[4] = register_count >> 8; + frame[5] = register_count >> 0; + auto crc = crc16(frame, 6); + frame[6] = crc >> 0; + frame[7] = crc >> 8; + + this->write_array(frame, 8); +} + +} // namespace modbus +} // namespace esphome diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h new file mode 100644 index 0000000000..b75de147b1 --- /dev/null +++ b/esphome/components/modbus/modbus.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace modbus { + +class ModbusDevice; + +class Modbus : public uart::UARTDevice, public Component { + public: + Modbus() = default; + + void loop() override; + + void dump_config() override; + + void register_device(ModbusDevice *device) { this->devices_.push_back(device); } + + float get_setup_priority() const override; + + void send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count); + + protected: + bool parse_modbus_byte_(uint8_t byte); + + std::vector<uint8_t> rx_buffer_; + uint32_t last_modbus_byte_{0}; + std::vector<ModbusDevice *> devices_; +}; + +class ModbusDevice { + public: + void set_parent(Modbus *parent) { parent_ = parent; } + void set_address(uint8_t address) { address_ = address; } + virtual void on_modbus_data(const std::vector<uint8_t> &data) = 0; + + void send(uint8_t function, uint16_t start_address, uint16_t register_count) { + this->parent_->send(this->address_, function, start_address, register_count); + } + + protected: + friend Modbus; + + Modbus *parent_; + uint8_t address_; +}; + +} // namespace modbus +} // namespace esphome diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index a24a703306..2025bc5b3f 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -10,7 +10,7 @@ void MPR121Component::setup() { ESP_LOGCONFIG(TAG, "Setting up MPR121..."); // soft reset device this->write_byte(MPR121_SOFTRESET, 0x63); - delay(100); + delay(100); // NOLINT if (!this->write_byte(MPR121_ECR, 0x0)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index d30e25c187..073bb3cede 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -15,7 +15,7 @@ from esphome.const import CONF_AVAILABILITY, CONF_BIRTH_MESSAGE, CONF_BROKER, CO from esphome.core import coroutine_with_priority, coroutine, CORE DEPENDENCIES = ['network'] -AUTO_LOAD = ['json'] +AUTO_LOAD = ['json', 'async_tcp'] def validate_message_just_topic(value): @@ -121,7 +121,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_SSL_FINGERPRINTS): cv.All(cv.only_on_esp8266, cv.ensure_list(validate_fingerprint)), cv.Optional(CONF_KEEPALIVE, default='15s'): cv.positive_time_period_seconds, - cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds, cv.Optional(CONF_ON_MESSAGE): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTMessageTrigger), cv.Required(CONF_TOPIC): cv.subscribe_topic, @@ -154,7 +154,8 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) - cg.add_library('AsyncMqttClient', '0.8.2') + # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json + cg.add_library('AsyncMqttClient-esphome', '0.8.3') cg.add_define('USE_MQTT') cg.add_global(mqtt_ns.using) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index e07204d559..2eb1c52153 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -201,7 +201,7 @@ void MQTTClientComponent::check_connected() { this->status_clear_warning(); ESP_LOGI(TAG, "MQTT Connected!"); // MQTT Client needs some time to be fully set up. - delay(100); + delay(100); // NOLINT this->resubscribe_subscriptions_(); diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 8085fbf0f2..48b470cfb2 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -141,7 +141,21 @@ std::string MQTTClimateComponent::friendly_name() const { return this->device_-> bool MQTTClimateComponent::publish_state_() { auto traits = this->device_->get_traits(); // mode - const char *mode_s = climate_mode_to_string(this->device_->mode); + const char *mode_s = ""; + switch (this->device_->mode) { + case CLIMATE_MODE_OFF: + mode_s = "off"; + break; + case CLIMATE_MODE_AUTO: + mode_s = "auto"; + break; + case CLIMATE_MODE_COOL: + mode_s = "cool"; + break; + case CLIMATE_MODE_HEAT: + mode_s = "heat"; + break; + } bool success = true; if (!this->publish(this->get_mode_state_topic(), mode_s)) success = false; diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 53c4e89e98..4201d41c44 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -149,9 +149,6 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai } void MQTTComponent::disable_availability() { this->set_availability("", "", ""); } void MQTTComponent::call_setup() { - // Call component internal setup. - this->setup_internal_(); - if (this->is_internal()) return; @@ -173,8 +170,6 @@ void MQTTComponent::call_setup() { } void MQTTComponent::call_loop() { - this->loop_internal_(); - if (this->is_internal()) return; diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 56d18a3d22..a414c261f0 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -73,6 +73,9 @@ void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCon root["tilt_status_topic"] = this->get_tilt_state_topic(); root["tilt_command_topic"] = this->get_tilt_command_topic(); } + if (traits.get_supports_tilt() && !traits.get_supports_position()) { + config.command_topic = false; + } } std::string MQTTCoverComponent::component_type() const { return "cover"; } diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index a241cf6ed6..f87e7651b9 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -55,6 +55,9 @@ void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCo if (!this->sensor_->get_icon().empty()) root["icon"] = this->sensor_->get_icon(); + if (this->sensor_->get_force_update()) + root["force_update"] = true; + config.command_topic = false; } bool MQTTSensorComponent::send_initial_state() { diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index e4c08c8e4e..37d475d25d 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -15,9 +15,6 @@ void MQTTTextSensor::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig if (!this->sensor_->get_icon().empty()) root["icon"] = this->sensor_->get_icon(); - if (!this->sensor_->unique_id().empty()) - root["unique_id"] = this->sensor_->unique_id(); - config.command_topic = false; } void MQTTTextSensor::setup() { @@ -40,6 +37,7 @@ bool MQTTTextSensor::send_initial_state() { bool MQTTTextSensor::is_internal() { return this->sensor_->is_internal(); } std::string MQTTTextSensor::component_type() const { return "sensor"; } std::string MQTTTextSensor::friendly_name() const { return this->sensor_->get_name(); } +std::string MQTTTextSensor::unique_id() { return this->sensor_->unique_id(); } } // namespace mqtt } // namespace esphome diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index 94afe30381..a5ce0658c7 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -31,6 +31,8 @@ class MQTTTextSensor : public mqtt::MQTTComponent { std::string friendly_name() const override; + std::string unique_id() override; + text_sensor::TextSensor *sensor_; }; diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 33ed6b1899..39bce9f32c 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -19,7 +19,7 @@ void MS5611Component::setup() { this->mark_failed(); return; } - delay(100); + delay(100); // NOLINT for (uint8_t offset = 0; offset < 6; offset++) { if (!this->read_byte_16(MS5611_CMD_READ_PROM + (offset * 2), &this->prom_[offset])) { this->mark_failed(); diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 694ac028fc..e5106d4bd6 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -101,6 +101,14 @@ ESP8266_METHODS = { ESP32_METHODS = { 'ESP32_I2S_0': 'NeoEsp32I2s0{}Method', 'ESP32_I2S_1': 'NeoEsp32I2s1{}Method', + 'ESP32_RMT_0': 'NeoEsp32Rmt0{}Method', + 'ESP32_RMT_1': 'NeoEsp32Rmt1{}Method', + 'ESP32_RMT_2': 'NeoEsp32Rmt2{}Method', + 'ESP32_RMT_3': 'NeoEsp32Rmt3{}Method', + 'ESP32_RMT_4': 'NeoEsp32Rmt4{}Method', + 'ESP32_RMT_5': 'NeoEsp32Rmt5{}Method', + 'ESP32_RMT_6': 'NeoEsp32Rmt6{}Method', + 'ESP32_RMT_7': 'NeoEsp32Rmt7{}Method', 'BIT_BANG': 'NeoEsp32BitBang{}Method', } @@ -160,4 +168,5 @@ def to_code(config): cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE]))) - cg.add_library('NeoPixelBus', '2.4.1') + # https://github.com/Makuna/NeoPixelBus/blob/master/library.json + cg.add_library('NeoPixelBus-esphome', '2.5.2') diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index f41a97ce7e..e594e147f4 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -46,7 +46,7 @@ void Nextion::set_component_value(const char *component, int value) { this->send_command_printf("%s.val=%d", component, value); } void Nextion::display_picture(int picture_id, int x_start, int y_start) { - this->send_command_printf("pic %d %d %d", picture_id, x_start, y_start); + this->send_command_printf("pic %d %d %d", x_start, y_start, picture_id); } void Nextion::set_component_background_color(const char *component, const char *color) { this->send_command_printf("%s.bco=\"%s\"", component, color); diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 92b41a88af..bd37e241e9 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -394,7 +394,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { bool wait_for_ack_{true}; }; -class NextionTouchComponent : public binary_sensor::BinarySensor { +class NextionTouchComponent : public binary_sensor::BinarySensorInitiallyOff { public: void set_page_id(uint8_t page_id) { page_id_ = page_id; } void set_component_id(uint8_t component_id) { component_id_ = component_id; } diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index 1b5c5182c7..9446508b0b 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -19,9 +19,9 @@ void NTC::process_(float value) { return; } - float lr = logf(value); - float v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr; - float temp = 1 / v - 273.15f; + double lr = log(double(value)); + double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr; + auto temp = float(1.0 / v - 273.15); ESP_LOGD(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp); this->publish_state(temp); diff --git a/esphome/components/ntc/ntc.h b/esphome/components/ntc/ntc.h index 9d6b37412d..c8592e0fe8 100644 --- a/esphome/components/ntc/ntc.h +++ b/esphome/components/ntc/ntc.h @@ -9,9 +9,9 @@ namespace ntc { class NTC : public Component, public sensor::Sensor { public: void set_sensor(Sensor *sensor) { sensor_ = sensor; } - void set_a(float a) { a_ = a; } - void set_b(float b) { b_ = b; } - void set_c(float c) { c_ = c; } + void set_a(double a) { a_ = a; } + void set_b(double b) { b_ = b; } + void set_c(double c) { c_ = c; } void setup() override; void dump_config() override; float get_setup_priority() const override; @@ -20,9 +20,9 @@ class NTC : public Component, public sensor::Sensor { void process_(float value); sensor::Sensor *sensor_; - float a_; - float b_; - float c_; + double a_; + double b_; + double c_; }; } // namespace ntc diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index e290e57baf..869de777d6 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -22,6 +22,8 @@ def to_code(config): cg.add(var.set_port(config[CONF_PORT])) cg.add(var.set_auth_password(config[CONF_PASSWORD])) + yield cg.register_component(var, config) + if config[CONF_SAFE_MODE]: cg.add(var.start_safe_mode()) @@ -29,6 +31,3 @@ def to_code(config): cg.add_library('Update', None) elif CORE.is_esp32: cg.add_library('Hash', None) - - # Register at end for safe mode - yield cg.register_component(var, config) diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index d37a7a0c6a..2041c688eb 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -182,11 +182,11 @@ void OTAComponent::handle_() { error_code = OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; goto error; } - if (ss.indexOf("new Flash config wrong") != -1) { + if (ss.indexOf("new Flash config wrong") != -1 || ss.indexOf("new Flash config wsong") != -1) { error_code = OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; goto error; } - if (ss.indexOf("Flash config wrong real") != -1) { + if (ss.indexOf("Flash config wrong real") != -1 || ss.indexOf("Flash config wsong real") != -1) { error_code = OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; goto error; } @@ -266,7 +266,7 @@ void OTAComponent::handle_() { delay(10); ESP_LOGI(TAG, "OTA update finished!"); this->status_clear_warning(); - delay(100); + delay(100); // NOLINT App.safe_reboot(); error: @@ -358,7 +358,7 @@ void OTAComponent::start_safe_mode(uint8_t num_attempts, uint32_t enable_time) { this->safe_mode_start_time_ = millis(); this->safe_mode_enable_time_ = enable_time; this->safe_mode_num_attempts_ = num_attempts; - this->rtc_ = global_preferences.make_preference<uint32_t>(233825507UL); + this->rtc_ = global_preferences.make_preference<uint32_t>(233825507UL, false); this->safe_mode_rtc_value_ = this->read_rtc_(); ESP_LOGCONFIG(TAG, "There have been %u suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_); @@ -369,19 +369,18 @@ void OTAComponent::start_safe_mode(uint8_t num_attempts, uint32_t enable_time) { ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode."); this->status_set_error(); - network_setup(); - this->call_setup(); + this->set_timeout(enable_time, []() { + ESP_LOGE(TAG, "No OTA attempt made, restarting."); + App.reboot(); + }); + + App.setup(); ESP_LOGI(TAG, "Waiting for OTA attempt."); - uint32_t begin = millis(); - while ((millis() - begin) < enable_time) { - this->call_loop(); - network_tick(); - App.feed_wdt(); - yield(); + + while (true) { + App.loop(); } - ESP_LOGE(TAG, "No OTA attempt made, restarting."); - App.reboot(); } else { // increment counter this->write_rtc_(this->safe_mode_rtc_value_ + 1); diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index d469cf835f..50922e2f48 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -55,8 +55,6 @@ void PCF8574Component::pin_mode(uint8_t pin, uint8_t mode) { default: break; } - - this->write_gpio_(); } bool PCF8574Component::read_gpio_() { if (this->is_failed()) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 548099a495..489442c637 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -169,6 +169,7 @@ void PMSX003Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_); + this->check_uart_settings(9600); } } // namespace pmsx003 diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index fa9a92d430..0cbaf1bf29 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -12,24 +12,24 @@ pmsx003_ns = cg.esphome_ns.namespace('pmsx003') PMSX003Component = pmsx003_ns.class_('PMSX003Component', uart.UARTDevice, cg.Component) PMSX003Sensor = pmsx003_ns.class_('PMSX003Sensor', sensor.Sensor) -CONF_PMSX003 = 'PMSX003' -CONF_PMS5003T = 'PMS5003T' -CONF_PMS5003ST = 'PMS5003ST' +TYPE_PMSX003 = 'PMSX003' +TYPE_PMS5003T = 'PMS5003T' +TYPE_PMS5003ST = 'PMS5003ST' PMSX003Type = pmsx003_ns.enum('PMSX003Type') PMSX003_TYPES = { - CONF_PMSX003: PMSX003Type.PMSX003_TYPE_X003, - CONF_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, - CONF_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST, + TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, + TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, + TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST, } SENSORS_TO_TYPE = { - CONF_PM_1_0: [CONF_PMSX003, CONF_PMS5003ST], - CONF_PM_2_5: [CONF_PMSX003, CONF_PMS5003T, CONF_PMS5003ST], - CONF_PM_10_0: [CONF_PMSX003, CONF_PMS5003ST], - CONF_TEMPERATURE: [CONF_PMS5003T, CONF_PMS5003ST], - CONF_HUMIDITY: [CONF_PMS5003T, CONF_PMS5003ST], - CONF_FORMALDEHYDE: [CONF_PMS5003ST], + CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003ST], + CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003ST], + CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_FORMALDEHYDE: [TYPE_PMS5003ST], } diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 07a41444ce..93000a7421 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -335,7 +335,6 @@ bool PN532::wait_ready_() { return true; } -bool PN532::is_device_msb_first() { return false; } void PN532::dump_config() { ESP_LOGCONFIG(TAG, "PN532:"); switch (this->error_code_) { diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index d349c7a150..49d5878265 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -11,7 +11,9 @@ namespace pn532 { class PN532BinarySensor; class PN532Trigger; -class PN532 : public PollingComponent, public spi::SPIDevice { +class PN532 : public PollingComponent, + public spi::SPIDevice<spi::BIT_ORDER_LSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, + spi::DATA_RATE_1MHZ> { public: void setup() override; @@ -26,8 +28,6 @@ class PN532 : public PollingComponent, public spi::SPIDevice { void register_trigger(PN532Trigger *trig) { this->triggers_.push_back(trig); } protected: - bool is_device_msb_first() override; - /// Write the full command given in data to the PN532 void pn532_write_command_(const std::vector<uint8_t> &data); bool pn532_write_command_check_ack_(const std::vector<uint8_t> &data); diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 6503711e35..c71e51eb32 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -149,6 +149,7 @@ void PulseCounterSensor::dump_config() { ESP_LOGCONFIG(TAG, " Rising Edge: %s", EDGE_MODE_TO_STRING[this->storage_.rising_edge_mode]); ESP_LOGCONFIG(TAG, " Falling Edge: %s", EDGE_MODE_TO_STRING[this->storage_.falling_edge_mode]); ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %u µs", this->storage_.filter_us); + LOG_UPDATE_INTERVAL(this); } void PulseCounterSensor::update() { diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 636147c138..e73bc36036 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import sensor from esphome.const import CONF_COUNT_MODE, CONF_FALLING_EDGE, CONF_ID, CONF_INTERNAL_FILTER, \ - CONF_PIN, CONF_RISING_EDGE, CONF_UPDATE_INTERVAL, CONF_NUMBER, \ + CONF_PIN, CONF_RISING_EDGE, CONF_NUMBER, \ ICON_PULSE, UNIT_PULSES_PER_MINUTE from esphome.core import CORE @@ -49,7 +49,6 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2).exte cv.Required(CONF_FALLING_EDGE): COUNT_MODE_SCHEMA, }), cv.Optional(CONF_INTERNAL_FILTER, default='13us'): validate_internal_filter, - cv.Optional(CONF_UPDATE_INTERVAL, default='60s'): cv.update_interval, }).extend(cv.polling_component_schema('60s')) diff --git a/esphome/components/pzem004t/__init__.py b/esphome/components/pzem004t/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp new file mode 100644 index 0000000000..cbdc14f0d0 --- /dev/null +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -0,0 +1,109 @@ +#include "pzem004t.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzem004t { + +static const char *TAG = "pzem004t"; + +void PZEM004T::loop() { + const uint32_t now = millis(); + if (now - this->last_read_ > 500 && this->available()) { + while (this->available()) + this->read(); + this->last_read_ = now; + } + + // PZEM004T packet size is 7 byte + while (this->available() >= 7) { + auto resp = *this->read_array<7>(); + // packet format: + // 0: packet type + // 1-5: data + // 6: checksum (sum of other bytes) + // see https://github.com/olehs/PZEM004T + uint8_t sum = 0; + for (int i = 0; i < 6; i++) + sum += resp[i]; + + if (sum != resp[6]) { + ESP_LOGV(TAG, "PZEM004T invalid checksum! 0x%02X != 0x%02X", sum, resp[6]); + continue; + } + + switch (resp[0]) { + case 0xA4: { // Set Module Address Response + this->write_state_(READ_VOLTAGE); + break; + } + case 0xA0: { // Voltage Response + uint16_t int_voltage = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + float voltage = int_voltage + (resp[3] / 10.0f); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + ESP_LOGD(TAG, "Got Voltage %.1f V", voltage); + this->write_state_(READ_CURRENT); + break; + } + case 0xA1: { // Current Response + uint16_t int_current = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + float current = int_current + (resp[3] / 100.0f); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + ESP_LOGD(TAG, "Got Current %.2f A", current); + this->write_state_(READ_POWER); + break; + } + case 0xA2: { // Active Power Response + uint16_t power = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(power); + ESP_LOGD(TAG, "Got Power %u W", power); + this->write_state_(DONE); + break; + } + + case 0xA3: // Energy Response + case 0xA5: // Set Power Alarm Response + case 0xB0: // Voltage Request + case 0xB1: // Current Request + case 0xB2: // Active Power Response + case 0xB3: // Energy Request + case 0xB4: // Set Module Address Request + case 0xB5: // Set Power Alarm Request + default: + break; + } + + this->last_read_ = now; + } +} +void PZEM004T::update() { this->write_state_(SET_ADDRESS); } +void PZEM004T::write_state_(PZEM004T::PZEM004TReadState state) { + if (state == DONE) { + this->read_state_ = state; + return; + } + std::array<uint8_t, 7> data{}; + data[0] = state; + data[1] = 192; + data[2] = 168; + data[3] = 1; + data[4] = 1; + data[5] = 0; + data[6] = 0; + for (int i = 0; i < 6; i++) + data[6] += data[i]; + + this->write_array(data); + this->read_state_ = state; +} +void PZEM004T::dump_config() { + ESP_LOGCONFIG(TAG, "PZEM004T:"); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); +} + +} // namespace pzem004t +} // namespace esphome diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h new file mode 100644 index 0000000000..f0208d415a --- /dev/null +++ b/esphome/components/pzem004t/pzem004t.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace pzem004t { + +class PZEM004T : public PollingComponent, public uart::UARTDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + + void loop() override; + + void update() override; + + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + + enum PZEM004TReadState { + SET_ADDRESS = 0xB4, + READ_VOLTAGE = 0xB0, + READ_CURRENT = 0xB1, + READ_POWER = 0xB2, + DONE = 0x00, + } read_state_{DONE}; + + void write_state_(PZEM004TReadState state); + + uint32_t last_read_{0}; +}; + +} // namespace pzem004t +} // namespace esphome diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py new file mode 100644 index 0000000000..6e3628c5ec --- /dev/null +++ b/esphome/components/pzem004t/sensor.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT + +DEPENDENCIES = ['uart'] + +pzem004t_ns = cg.esphome_ns.namespace('pzem004t') +PZEM004T = pzem004t_ns.class_('PZEM004T', cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEM004T), + + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 0), +}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/pzemac/__init__.py b/esphome/components/pzemac/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp new file mode 100644 index 0000000000..f05ce15711 --- /dev/null +++ b/esphome/components/pzemac/pzemac.cpp @@ -0,0 +1,71 @@ +#include "pzemac.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzemac { + +static const char *TAG = "pzemac"; + +static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers + +void PZEMAC::on_modbus_data(const std::vector<uint8_t> &data) { + if (data.size() < 20) { + ESP_LOGW(TAG, "Invalid size for PZEM AC!"); + return; + } + + // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 + // 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34 + // Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc-- + + auto pzem_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); + }; + auto pzem_get_32bit = [&](size_t i) -> uint32_t { + return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0); + }; + + uint16_t raw_voltage = pzem_get_16bit(0); + float voltage = raw_voltage / 10.0f; // max 6553.5 V + + uint32_t raw_current = pzem_get_32bit(2); + float current = raw_current / 1000.0f; // max 4294967.295 A + + uint32_t raw_active_power = pzem_get_32bit(6); + float active_power = raw_active_power / 10.0f; // max 429496729.5 W + + uint16_t raw_frequency = pzem_get_16bit(14); + float frequency = raw_frequency / 10.0f; + + uint16_t raw_power_factor = pzem_get_16bit(16); + float power_factor = raw_power_factor / 100.0f; + + ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, F=%.1f Hz, PF=%.2f", voltage, current, active_power, frequency, + power_factor); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(active_power); + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + if (this->power_factor_sensor_ != nullptr) + this->power_factor_sensor_->publish_state(power_factor); +} + +void PZEMAC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, PZEM_REGISTER_COUNT); } +void PZEMAC::dump_config() { + ESP_LOGCONFIG(TAG, "PZEMAC:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); + LOG_SENSOR("", "Frequency", this->frequency_sensor_); + LOG_SENSOR("", "Power Factor", this->power_factor_sensor_); +} + +} // namespace pzemac +} // namespace esphome diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h new file mode 100644 index 0000000000..d396b7cddf --- /dev/null +++ b/esphome/components/pzemac/pzemac.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace pzemac { + +class PZEMAC : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } + + void update() override; + + void on_modbus_data(const std::vector<uint8_t> &data) override; + + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + sensor::Sensor *frequency_sensor_; + sensor::Sensor *power_factor_sensor_; +}; + +} // namespace pzemac +} // namespace esphome diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py new file mode 100644 index 0000000000..35d8069767 --- /dev/null +++ b/esphome/components/pzemac/sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + CONF_FREQUENCY, UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, \ + ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC + +AUTO_LOAD = ['modbus'] + +pzemac_ns = cg.esphome_ns.namespace('pzemac') +PZEMAC = pzemac_ns.class_('PZEMAC', cg.PollingComponent, modbus.ModbusDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEMAC), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_EMPTY, ICON_CURRENT_AC, 1), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), +}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield modbus.register_modbus_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) + if CONF_FREQUENCY in config: + conf = config[CONF_FREQUENCY] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_frequency_sensor(sens)) + if CONF_POWER_FACTOR in config: + conf = config[CONF_POWER_FACTOR] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_factor_sensor(sens)) diff --git a/esphome/components/pzemdc/__init__.py b/esphome/components/pzemdc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp new file mode 100644 index 0000000000..9bd58410c0 --- /dev/null +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -0,0 +1,59 @@ +#include "pzemdc.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzemdc { + +static const char *TAG = "pzemdc"; + +static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers + +void PZEMDC::on_modbus_data(const std::vector<uint8_t> &data) { + if (data.size() < 16) { + ESP_LOGW(TAG, "Invalid size for PZEM DC!"); + return; + } + + // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 + // 0 1 2 3 4 5 6 7 = ModBus register + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 = Buffer index + // 01 04 10 05 40 00 0A 00 0D 00 00 00 02 00 00 00 00 00 00 D6 29 + // Id Cc Sz Volt- Curre Power------ Energy----- HiAlm LoAlm Crc-- + + auto pzem_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); + }; + auto pzem_get_32bit = [&](size_t i) -> uint32_t { + return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0); + }; + + uint16_t raw_voltage = pzem_get_16bit(0); + float voltage = raw_voltage / 100.0f; // max 655.35 V + + uint16_t raw_current = pzem_get_16bit(2); + float current = raw_current / 100.0f; // max 655.35 A + + uint32_t raw_power = pzem_get_32bit(4); + float power = raw_power / 10.0f; // max 429496729.5 W + + ESP_LOGD(TAG, "PZEM DC: V=%.1f V, I=%.3f A, P=%.1f W", voltage, current, power); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(power); +} + +void PZEMDC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, 8); } +void PZEMDC::dump_config() { + ESP_LOGCONFIG(TAG, "PZEMDC:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); +} + +} // namespace pzemdc +} // namespace esphome diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h new file mode 100644 index 0000000000..d838eb4167 --- /dev/null +++ b/esphome/components/pzemdc/pzemdc.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace pzemdc { + +class PZEMDC : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_powerfactor_sensor(sensor::Sensor *powerfactor_sensor) { power_factor_sensor_ = powerfactor_sensor; } + + void update() override; + + void on_modbus_data(const std::vector<uint8_t> &data) override; + + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + sensor::Sensor *frequency_sensor_; + sensor::Sensor *power_factor_sensor_; +}; + +} // namespace pzemdc +} // namespace esphome diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py new file mode 100644 index 0000000000..8c6fd08868 --- /dev/null +++ b/esphome/components/pzemdc/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, ICON_POWER, ICON_CURRENT_AC + +AUTO_LOAD = ['modbus'] + +pzemdc_ns = cg.esphome_ns.namespace('pzemdc') +PZEMDC = pzemdc_ns.class_('PZEMDC', cg.PollingComponent, modbus.ModbusDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEMDC), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), +}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield modbus.register_modbus_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h index a67b6e7ce8..13df400754 100644 --- a/esphome/components/rdm6300/rdm6300.h +++ b/esphome/components/rdm6300/rdm6300.h @@ -28,7 +28,7 @@ class RDM6300Component : public Component, public uart::UARTDevice { uint32_t last_id_{0}; }; -class RDM6300BinarySensor : public binary_sensor::BinarySensor { +class RDM6300BinarySensor : public binary_sensor::BinarySensorInitiallyOff { public: void set_id(uint32_t id) { id_ = id; } diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index d3da238b08..a62304c87d 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -88,7 +88,7 @@ def validate_repeat(value): return cv.Schema({ cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), cv.Optional(CONF_WAIT_TIME, default='10ms'): - cv.templatable(cv.positive_time_period_milliseconds), + cv.templatable(cv.positive_time_period_microseconds), })(value) return validate_repeat({CONF_TIMES: value}) @@ -250,7 +250,7 @@ def lg_dumper(var, config): def lg_action(var, config, args): template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) - template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint8) + template_ = yield cg.templatable(config[CONF_NBITS], args, cg.uint8) cg.add(var.set_nbits(template_)) @@ -420,7 +420,7 @@ def rc5_action(var, config, args): RC_SWITCH_TIMING_SCHEMA = cv.All([cv.uint8_t], cv.Length(min=2, max=2)) RC_SWITCH_PROTOCOL_SCHEMA = cv.Any( - cv.int_range(min=1, max=7), + cv.int_range(min=1, max=8), cv.Schema({ cv.Required(CONF_PULSE_LENGTH): cv.uint32_t, cv.Optional(CONF_SYNC, default=[1, 31]): RC_SWITCH_TIMING_SCHEMA, @@ -438,14 +438,30 @@ def validate_rc_switch_code(value): if c not in ('0', '1'): raise cv.Invalid(u"Invalid RCSwitch code character '{}'. Only '0' and '1' are allowed" u"".format(c)) - if len(value) > 32: - raise cv.Invalid("Maximum length for RCSwitch codes is 32, code '{}' has length {}" + if len(value) > 64: + raise cv.Invalid("Maximum length for RCSwitch codes is 64, code '{}' has length {}" "".format(value, len(value))) if not value: raise cv.Invalid("RCSwitch code must not be empty") return value +def validate_rc_switch_raw_code(value): + if not isinstance(value, (str, text_type)): + raise cv.Invalid("All RCSwitch raw codes must be in quotes ('')") + for c in value: + if c not in ('0', '1', 'x'): + raise cv.Invalid( + "Invalid RCSwitch raw code character '{}'.Only '0', '1' and 'x' are allowed" + .format(c)) + if len(value) > 64: + raise cv.Invalid("Maximum length for RCSwitch raw codes is 64, code '{}' has length {}" + "".format(value, len(value))) + if not value: + raise cv.Invalid("RCSwitch raw code must not be empty") + return value + + def build_rc_switch_protocol(config): if isinstance(config, int): return rc_switch_protocols[config] @@ -457,7 +473,7 @@ def build_rc_switch_protocol(config): RC_SWITCH_RAW_SCHEMA = cv.Schema({ - cv.Required(CONF_CODE): validate_rc_switch_code, + cv.Required(CONF_CODE): validate_rc_switch_raw_code, cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, }) RC_SWITCH_TYPE_A_SCHEMA = cv.Schema({ @@ -490,7 +506,7 @@ RC_SWITCH_TRANSMITTER = cv.Schema({ cv.Optional(CONF_REPEAT, default={CONF_TIMES: 5}): cv.Schema({ cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), cv.Optional(CONF_WAIT_TIME, default='10ms'): - cv.templatable(cv.positive_time_period_milliseconds), + cv.templatable(cv.positive_time_period_microseconds), }), }) @@ -624,7 +640,7 @@ def samsung_dumper(var, config): @register_action('samsung', SamsungAction, SAMSUNG_SCHEMA) def samsung_action(var, config, args): - template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint16) + template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 029f1fccf2..754b2fae49 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -6,14 +6,15 @@ namespace remote_base { static const char *TAG = "remote.rc_switch"; -RCSwitchBase rc_switch_protocols[8] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false), +RCSwitchBase rc_switch_protocols[9] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false), RCSwitchBase(350, 10850, 350, 1050, 1050, 350, false), RCSwitchBase(650, 6500, 650, 1300, 1300, 650, false), RCSwitchBase(3000, 7100, 400, 1100, 900, 600, false), RCSwitchBase(380, 2280, 380, 1140, 1140, 380, false), RCSwitchBase(3000, 7000, 500, 1000, 1000, 500, false), RCSwitchBase(10350, 450, 450, 900, 900, 450, true), - RCSwitchBase(300, 9300, 150, 900, 900, 150, false)}; + RCSwitchBase(300, 9300, 150, 900, 900, 150, false), + RCSwitchBase(250, 2500, 250, 1250, 250, 250, false)}; RCSwitchBase::RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high, uint32_t one_low, bool inverted) @@ -52,7 +53,7 @@ void RCSwitchBase::sync(RemoteTransmitData *dst) const { dst->mark(this->sync_low_); } } -void RCSwitchBase::transmit(RemoteTransmitData *dst, uint32_t code, uint8_t len) const { +void RCSwitchBase::transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const { dst->set_carrier_frequency(0); for (int16_t i = len - 1; i >= 0; i--) { if (code & (1 << i)) @@ -108,12 +109,12 @@ bool RCSwitchBase::expect_sync(RemoteReceiveData &src) const { src.advance(2); return true; } -bool RCSwitchBase::decode(RemoteReceiveData &src, uint32_t *out_data, uint8_t *out_nbits) const { +bool RCSwitchBase::decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *out_nbits) const { // ignore if sync doesn't exist this->expect_sync(src); *out_data = 0; - for (*out_nbits = 1; *out_nbits < 32; *out_nbits += 1) { + for (*out_nbits = 0; *out_nbits < 64; *out_nbits += 1) { if (this->expect_zero(src)) { *out_data <<= 1; *out_data |= 0; @@ -121,14 +122,13 @@ bool RCSwitchBase::decode(RemoteReceiveData &src, uint32_t *out_data, uint8_t *o *out_data <<= 1; *out_data |= 1; } else { - *out_nbits -= 1; return *out_nbits >= 8; } } return true; } -void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint32_t *out_code) { +void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_t *out_code) { *out_code = 0; for (int8_t i = nbits - 1; i >= 0; i--) { *out_code <<= 2; @@ -138,24 +138,18 @@ void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint32_ *out_code |= 0b00; } } -void RCSwitchBase::type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint32_t *out_code, +void RCSwitchBase::type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint64_t *out_code, uint8_t *out_nbits) { uint16_t code = 0; - code |= (switch_group & 0b0001) ? 0 : 0b1000; - code |= (switch_group & 0b0010) ? 0 : 0b0100; - code |= (switch_group & 0b0100) ? 0 : 0b0010; - code |= (switch_group & 0b1000) ? 0 : 0b0001; - code <<= 4; - code |= (switch_device & 0b0001) ? 0 : 0b1000; - code |= (switch_device & 0b0010) ? 0 : 0b0100; - code |= (switch_device & 0b0100) ? 0 : 0b0010; - code |= (switch_device & 0b1000) ? 0 : 0b0001; + code = switch_group ^ 0b11111; + code <<= 5; + code |= switch_device ^ 0b11111; code <<= 2; code |= state ? 0b01 : 0b10; - simple_code_to_tristate(code, 10, out_code); - *out_nbits = 20; + simple_code_to_tristate(code, 12, out_code); + *out_nbits = 24; } -void RCSwitchBase::type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint32_t *out_code, +void RCSwitchBase::type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint64_t *out_code, uint8_t *out_nbits) { uint16_t code = 0; code |= (address_code == 1) ? 0 : 0b1000; @@ -173,7 +167,7 @@ void RCSwitchBase::type_b_code(uint8_t address_code, uint8_t channel_code, bool simple_code_to_tristate(code, 12, out_code); *out_nbits = 24; } -void RCSwitchBase::type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint32_t *out_code, +void RCSwitchBase::type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint64_t *out_code, uint8_t *out_nbits) { uint16_t code = 0; code |= (family & 0b0001) ? 0b1000 : 0; @@ -191,7 +185,7 @@ void RCSwitchBase::type_c_code(uint8_t family, uint8_t group, uint8_t device, bo simple_code_to_tristate(code, 12, out_code); *out_nbits = 24; } -void RCSwitchBase::type_d_code(uint8_t group, uint8_t device, bool state, uint32_t *out_code, uint8_t *out_nbits) { +void RCSwitchBase::type_d_code(uint8_t group, uint8_t device, bool state, uint64_t *out_code, uint8_t *out_nbits) { *out_code = 0; *out_code |= (group == 0) ? 0b11000000 : 0b01000000; *out_code |= (group == 1) ? 0b00110000 : 0b00010000; @@ -208,8 +202,8 @@ void RCSwitchBase::type_d_code(uint8_t group, uint8_t device, bool state, uint32 *out_nbits = 24; } -uint32_t decode_binary_string(const std::string &data) { - uint32_t ret = 0; +uint64_t decode_binary_string(const std::string &data) { + uint64_t ret = 0; for (char c : data) { ret <<= 1UL; ret |= (c != '0'); @@ -217,22 +211,31 @@ uint32_t decode_binary_string(const std::string &data) { return ret; } +uint64_t decode_binary_string_mask(const std::string &data) { + uint64_t ret = 0; + for (char c : data) { + ret <<= 1UL; + ret |= (c != 'x'); + } + return ret; +} + bool RCSwitchRawReceiver::matches(RemoteReceiveData src) { - uint32_t decoded_code; + uint64_t decoded_code; uint8_t decoded_nbits; if (!this->protocol_.decode(src, &decoded_code, &decoded_nbits)) return false; - return decoded_nbits == this->nbits_ && decoded_code == this->code_; + return decoded_nbits == this->nbits_ && (decoded_code & this->mask_) == (this->code_ & this->mask_); } bool RCSwitchDumper::dump(RemoteReceiveData src) { - for (uint8_t i = 1; i <= 7; i++) { + for (uint8_t i = 1; i <= 8; i++) { src.reset(); - uint32_t out_data; + uint64_t out_data; uint8_t out_nbits; RCSwitchBase *protocol = &rc_switch_protocols[i]; if (protocol->decode(src, &out_data, &out_nbits) && out_nbits >= 3) { - char buffer[33]; + char buffer[65]; for (uint8_t j = 0; j < out_nbits; j++) buffer[j] = (out_data & (1 << (out_nbits - j - 1))) ? '1' : '0'; diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 728561c140..0983da27ea 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -18,7 +18,7 @@ class RCSwitchBase { void sync(RemoteTransmitData *dst) const; - void transmit(RemoteTransmitData *dst, uint32_t code, uint8_t len) const; + void transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const; bool expect_one(RemoteReceiveData &src) const; @@ -26,20 +26,20 @@ class RCSwitchBase { bool expect_sync(RemoteReceiveData &src) const; - bool decode(RemoteReceiveData &src, uint32_t *out_data, uint8_t *out_nbits) const; + bool decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *out_nbits) const; - static void simple_code_to_tristate(uint16_t code, uint8_t nbits, uint32_t *out_code); + static void simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_t *out_code); - static void type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint32_t *out_code, + static void type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint64_t *out_code, uint8_t *out_nbits); - static void type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint32_t *out_code, + static void type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint64_t *out_code, uint8_t *out_nbits); - static void type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint32_t *out_code, + static void type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint64_t *out_code, uint8_t *out_nbits); - static void type_d_code(uint8_t group, uint8_t device, bool state, uint32_t *out_code, uint8_t *out_nbits); + static void type_d_code(uint8_t group, uint8_t device, bool state, uint64_t *out_code, uint8_t *out_nbits); protected: uint32_t sync_high_{}; @@ -51,9 +51,11 @@ class RCSwitchBase { bool inverted_{}; }; -extern RCSwitchBase rc_switch_protocols[8]; +extern RCSwitchBase rc_switch_protocols[9]; -uint32_t decode_binary_string(const std::string &data); +uint64_t decode_binary_string(const std::string &data); + +uint64_t decode_binary_string_mask(const std::string &data); template<typename... Ts> class RCSwitchRawAction : public RemoteTransmitterActionBase<Ts...> { public: @@ -62,7 +64,7 @@ template<typename... Ts> class RCSwitchRawAction : public RemoteTransmitterActio void encode(RemoteTransmitData *dst, Ts... x) override { auto code = this->code_.value(x...); - uint32_t the_code = decode_binary_string(code); + uint64_t the_code = decode_binary_string(code); uint8_t nbits = code.size(); auto proto = this->protocol_.value(x...); @@ -84,7 +86,7 @@ template<typename... Ts> class RCSwitchTypeAAction : public RemoteTransmitterAct uint8_t u_group = decode_binary_string(group); uint8_t u_device = decode_binary_string(device); - uint32_t code; + uint64_t code; uint8_t nbits; RCSwitchBase::type_a_code(u_group, u_device, state, &code, &nbits); @@ -105,7 +107,7 @@ template<typename... Ts> class RCSwitchTypeBAction : public RemoteTransmitterAct auto channel = this->channel_.value(x...); auto state = this->state_.value(x...); - uint32_t code; + uint64_t code; uint8_t nbits; RCSwitchBase::type_b_code(address, channel, state, &code, &nbits); @@ -130,7 +132,7 @@ template<typename... Ts> class RCSwitchTypeCAction : public RemoteTransmitterAct auto u_family = static_cast<uint8_t>(tolower(family[0]) - 'a'); - uint32_t code; + uint64_t code; uint8_t nbits; RCSwitchBase::type_c_code(u_family, group, device, state, &code, &nbits); @@ -152,7 +154,7 @@ template<typename... Ts> class RCSwitchTypeDAction : public RemoteTransmitterAct auto u_group = static_cast<uint8_t>(tolower(group[0]) - 'a'); - uint32_t code; + uint64_t code; uint8_t nbits; RCSwitchBase::type_d_code(u_group, device, state, &code, &nbits); @@ -164,9 +166,10 @@ template<typename... Ts> class RCSwitchTypeDAction : public RemoteTransmitterAct class RCSwitchRawReceiver : public RemoteReceiverBinarySensorBase { public: void set_protocol(const RCSwitchBase &a_protocol) { this->protocol_ = a_protocol; } - void set_code(uint32_t code) { this->code_ = code; } + void set_code(uint64_t code) { this->code_ = code; } void set_code(const std::string &code) { this->code_ = decode_binary_string(code); + this->mask_ = decode_binary_string_mask(code); this->nbits_ = code.size(); } void set_nbits(uint8_t nbits) { this->nbits_ = nbits; } @@ -191,7 +194,8 @@ class RCSwitchRawReceiver : public RemoteReceiverBinarySensorBase { bool matches(RemoteReceiveData src) override; RCSwitchBase protocol_; - uint32_t code_; + uint64_t code_; + uint64_t mask_{0xFFFFFFFFFFFFFFFF}; uint8_t nbits_; }; diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 6035e2fd57..36be25add7 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -267,11 +267,11 @@ class RemoteReceiverBase : public RemoteComponentBase { uint8_t tolerance_{25}; }; -class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensor, +class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitiallyOff, public Component, public RemoteReceiverListener { public: - explicit RemoteReceiverBinarySensorBase() : BinarySensor() {} + explicit RemoteReceiverBinarySensorBase() : BinarySensorInitiallyOff() {} void dump_config() override; virtual bool matches(RemoteReceiveData src) = 0; bool on_receive(RemoteReceiveData src) override { diff --git a/esphome/components/restart/restart_switch.cpp b/esphome/components/restart/restart_switch.cpp index cd0979b9d3..f66ebc616e 100644 --- a/esphome/components/restart/restart_switch.cpp +++ b/esphome/components/restart/restart_switch.cpp @@ -13,8 +13,8 @@ void RestartSwitch::write_state(bool state) { if (state) { ESP_LOGI(TAG, "Restarting device..."); - // then execute - delay(100); // Let MQTT settle a bit + // Let MQTT settle a bit + delay(100); // NOLINT App.safe_reboot(); } } diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index b627a4e57f..4220645478 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/esphal.h" +#include "esphome/core/automation.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -43,6 +44,12 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { */ void set_resolution(RotaryEncoderResolution mode); + /// Manually set the value of the counter. + void set_value(int value) { + this->store_.counter = value; + this->loop(); + } + void set_reset_pin(GPIOPin *pin_i) { this->pin_i_ = pin_i; } void set_min_value(int32_t min_value); void set_max_value(int32_t max_value); @@ -63,5 +70,15 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { RotaryEncoderSensorStore store_{}; }; +template<typename... Ts> class RotaryEncoderSetValueAction : public Action<Ts...> { + public: + RotaryEncoderSetValueAction(RotaryEncoderSensor *encoder) : encoder_(encoder) {} + TEMPLATABLE_VALUE(int, value) + void play(Ts... x) override { this->encoder_->set_value(this->value_.value(x...)); } + + protected: + RotaryEncoderSensor *encoder_; +}; + } // namespace rotary_encoder } // namespace esphome diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index fb4881e18b..214ccbd056 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -1,9 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins +from esphome import pins, automation from esphome.components import sensor from esphome.const import CONF_ID, CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, UNIT_STEPS, \ - ICON_ROTATE_RIGHT + ICON_ROTATE_RIGHT, CONF_VALUE, CONF_PIN_A, CONF_PIN_B rotary_encoder_ns = cg.esphome_ns.namespace('rotary_encoder') RotaryEncoderResolution = rotary_encoder_ns.enum('RotaryEncoderResolution') @@ -13,11 +13,11 @@ RESOLUTIONS = { 4: RotaryEncoderResolution.ROTARY_ENCODER_4_PULSES_PER_CYCLE, } -CONF_PIN_A = 'pin_a' -CONF_PIN_B = 'pin_b' CONF_PIN_RESET = 'pin_reset' RotaryEncoderSensor = rotary_encoder_ns.class_('RotaryEncoderSensor', sensor.Sensor, cg.Component) +RotaryEncoderSetValueAction = rotary_encoder_ns.class_('RotaryEncoderSetValueAction', + automation.Action) def validate_min_max_value(config): @@ -60,3 +60,16 @@ def to_code(config): cg.add(var.set_min_value(config[CONF_MIN_VALUE])) if CONF_MAX_VALUE in config: cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + + +@automation.register_action('sensor.rotary_encoder.set_value', RotaryEncoderSetValueAction, + cv.Schema({ + cv.Required(CONF_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_VALUE): cv.templatable(cv.int_), + })) +def sensor_template_publish_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_VALUE], args, int) + cg.add(var.set_value(template_)) + yield var diff --git a/esphome/components/scd30/__init__.py b/esphome/components/scd30/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp new file mode 100644 index 0000000000..55ab07879e --- /dev/null +++ b/esphome/components/scd30/scd30.cpp @@ -0,0 +1,181 @@ +#include "scd30.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace scd30 { + +static const char *TAG = "scd30"; + +static const uint16_t SCD30_CMD_GET_FIRMWARE_VERSION = 0xd100; +static const uint16_t SCD30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010; +static const uint16_t SCD30_CMD_GET_DATA_READY_STATUS = 0x0202; +static const uint16_t SCD30_CMD_READ_MEASUREMENT = 0x0300; + +/// Commands for future use +static const uint16_t SCD30_CMD_STOP_MEASUREMENTS = 0x0104; +static const uint16_t SCD30_CMD_MEASUREMENT_INTERVAL = 0x4600; +static const uint16_t SCD30_CMD_AUTOMATIC_SELF_CALIBRATION = 0x5306; +static const uint16_t SCD30_CMD_FORCED_CALIBRATION = 0x5204; +static const uint16_t SCD30_CMD_TEMPERATURE_OFFSET = 0x5403; +static const uint16_t SCD30_CMD_ALTITUDE_COMPENSATION = 0x5102; +static const uint16_t SCD30_CMD_SOFT_RESET = 0xD304; + +void SCD30Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up scd30..."); + + /// Firmware version identification + if (!this->write_command_(SCD30_CMD_GET_FIRMWARE_VERSION)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + uint16_t raw_firmware_version[3]; + + if (!this->read_data_(raw_firmware_version, 3)) { + this->error_code_ = FIRMWARE_IDENTIFICATION_FAILED; + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "SCD30 Firmware v%0d.%02d", (uint16_t(raw_firmware_version[0]) >> 8), + uint16_t(raw_firmware_version[0] & 0xFF)); + + /// Sensor initialization + if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS)) { + ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } +} + +void SCD30Component::dump_config() { + ESP_LOGCONFIG(TAG, "scd30:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement Initialization failed!"); + break; + case FIRMWARE_IDENTIFICATION_FAILED: + ESP_LOGW(TAG, "Unable to read sensor firmware version"); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void SCD30Component::update() { + /// Check if measurement is ready before reading the value + if (!this->write_command_(SCD30_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + return; + } + + uint16_t raw_read_status[1]; + if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + this->status_set_warning(); + ESP_LOGW(TAG, "Data not ready yet!"); + return; + } + + if (!this->write_command_(SCD30_CMD_READ_MEASUREMENT)) { + ESP_LOGW(TAG, "Error reading measurement!"); + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[6]; + if (!this->read_data_(raw_data, 6)) { + this->status_set_warning(); + return; + } + + union uint32_float_t { + uint32_t uint32; + float value; + }; + uint32_t temp_c_o2_u32 = (((uint32_t(raw_data[0])) << 16) | (uint32_t(raw_data[1]))); + uint32_float_t co2{.uint32 = temp_c_o2_u32}; + + uint32_t temp_temp_u32 = (((uint32_t(raw_data[2])) << 16) | (uint32_t(raw_data[3]))); + uint32_float_t temperature{.uint32 = temp_temp_u32}; + + uint32_t temp_hum_u32 = (((uint32_t(raw_data[4])) << 16) | (uint32_t(raw_data[5]))); + uint32_float_t humidity{.uint32 = temp_hum_u32}; + + ESP_LOGD(TAG, "Got CO2=%.2fppm temperature=%.2f°C humidity=%.2f%%", co2.value, temperature.value, humidity.value); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2.value); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature.value); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity.value); + + this->status_clear_warning(); + }); +} + +bool SCD30Component::write_command_(uint16_t command) { + // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. + return this->write_byte(command >> 8, command & 0xFF); +} + +uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) { + uint8_t bit; + uint8_t crc = 0xFF; + + crc ^= data1; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + crc ^= data2; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + return crc; +} + +bool SCD30Component::read_data_(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + return false; + } + + for (uint8_t i = 0; i < len; i++) { + const uint8_t j = 3 * i; + uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + if (crc != buf[j + 2]) { + ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +} // namespace scd30 +} // namespace esphome diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h new file mode 100644 index 0000000000..999e66414d --- /dev/null +++ b/esphome/components/scd30/scd30.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace scd30 { + +/// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors. +class SCD30Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } + void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + + enum ErrorCode { + COMMUNICATION_FAILED, + FIRMWARE_IDENTIFICATION_FAILED, + MEASUREMENT_INIT_FAILED, + UNKNOWN + } error_code_{UNKNOWN}; + + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; +}; + +} // namespace scd30 +} // namespace esphome diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py new file mode 100644 index 0000000000..7a60725276 --- /dev/null +++ b/esphome/components/scd30/sensor.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, UNIT_PARTS_PER_MILLION, \ + CONF_HUMIDITY, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ + UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT, CONF_CO2 + +DEPENDENCIES = ['i2c'] + +scd30_ns = cg.esphome_ns.namespace('scd30') +SCD30Component = scd30_ns.class_('SCD30Component', cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SCD30Component), + cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, + ICON_PERIODIC_TABLE_CO2, 0), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x61)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_CO2 in config: + sens = yield sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index e1983689a6..9590679f83 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -8,6 +8,7 @@ script_ns = cg.esphome_ns.namespace('script') Script = script_ns.class_('Script', automation.Trigger.template()) ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action) ScriptStopAction = script_ns.class_('ScriptStopAction', automation.Action) +ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action) IsRunningCondition = script_ns.class_('IsRunningCondition', automation.Condition) CONFIG_SCHEMA = automation.validate_automation({ @@ -42,6 +43,14 @@ def script_stop_action_to_code(config, action_id, template_arg, args): yield cg.new_Pvariable(action_id, template_arg, paren) +@automation.register_action('script.wait', ScriptWaitAction, maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(Script) +})) +def script_wait_action_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(action_id, template_arg, paren) + + @automation.register_condition('script.is_running', IsRunningCondition, automation.maybe_simple_id({ cv.Required(CONF_ID): cv.use_id(Script) })) diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index f937b9d637..3b97327da8 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -49,5 +49,47 @@ template<typename... Ts> class IsRunningCondition : public Condition<Ts...> { Script *parent_; }; +template<typename... Ts> class ScriptWaitAction : public Action<Ts...>, public Component { + public: + ScriptWaitAction(Script *script) : script_(script) {} + + void play(Ts... x) { /* ignore - see play_complex */ + } + + void play_complex(Ts... x) override { + // Check if we can continue immediately. + if (!this->script_->is_running()) { + this->triggered_ = false; + this->play_next(x...); + return; + } + this->var_ = std::make_tuple(x...); + this->triggered_ = true; + this->loop(); + } + + void stop() override { this->triggered_ = false; } + + void loop() override { + if (!this->triggered_) + return; + + if (this->script_->is_running()) + return; + + this->triggered_ = false; + this->play_next_tuple(this->var_); + } + + float get_setup_priority() const override { return setup_priority::DATA; } + + bool is_running() override { return this->triggered_ || this->is_running_next(); } + + protected: + Script *script_; + bool triggered_{false}; + std::tuple<Ts...> var_{}; +}; + } // namespace script } // namespace esphome diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 1abb6210ce..1a5be0adc3 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -56,6 +56,7 @@ void SDS011Component::dump_config() { ESP_LOGCONFIG(TAG, " RX-only mode: %s", ONOFF(this->rx_mode_only_)); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + this->check_uart_settings(9600); } void SDS011Component::loop() { @@ -94,7 +95,6 @@ float SDS011Component::get_setup_priority() const { return setup_priority::DATA; void SDS011Component::set_rx_mode_only(bool rx_mode_only) { this->rx_mode_only_ = rx_mode_only; } void SDS011Component::sds011_write_command_(const uint8_t *command_data) { - this->flush(); this->write_byte(SDS011_MSG_HEAD); this->write_byte(SDS011_COMMAND_ID_REQUEST); this->write_array(command_data, SDS011_DATA_REQUEST_LENGTH); diff --git a/esphome/components/senseair/__init__.py b/esphome/components/senseair/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp new file mode 100644 index 0000000000..8b41a441ad --- /dev/null +++ b/esphome/components/senseair/senseair.cpp @@ -0,0 +1,80 @@ +#include "senseair.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace senseair { + +static const char *TAG = "senseair"; +static const uint8_t SENSEAIR_REQUEST_LENGTH = 8; +static const uint8_t SENSEAIR_RESPONSE_LENGTH = 13; +static const uint8_t SENSEAIR_COMMAND_GET_PPM[] = {0xFE, 0x04, 0x00, 0x00, 0x00, 0x04, 0xE5, 0xC6}; + +void SenseAirComponent::update() { + uint8_t response[SENSEAIR_RESPONSE_LENGTH]; + if (!this->senseair_write_command_(SENSEAIR_COMMAND_GET_PPM, response)) { + ESP_LOGW(TAG, "Reading data from SenseAir failed!"); + this->status_set_warning(); + return; + } + + if (response[0] != 0xFE || response[1] != 0x04) { + ESP_LOGW(TAG, "Invalid preamble from SenseAir!"); + this->status_set_warning(); + return; + } + + uint16_t calc_checksum = this->senseair_checksum_(response, 11); + uint16_t resp_checksum = (uint16_t(response[12]) << 8) | response[11]; + if (resp_checksum != calc_checksum) { + ESP_LOGW(TAG, "SenseAir checksum doesn't match: 0x%02X!=0x%02X", resp_checksum, calc_checksum); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + const uint8_t length = response[2]; + const uint16_t status = (uint16_t(response[3]) << 8) | response[4]; + const uint16_t ppm = (uint16_t(response[length + 1]) << 8) | response[length + 2]; + + ESP_LOGD(TAG, "SenseAir Received COâ‚‚=%uppm Status=0x%02X", ppm, status); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); +} + +uint16_t SenseAirComponent::senseair_checksum_(uint8_t *ptr, uint8_t length) { + uint16_t crc = 0xFFFF; + uint8_t i; + while (length--) { + crc ^= *ptr++; + for (i = 0; i < 8; i++) { + if ((crc & 0x01) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; +} + +bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t *response) { + this->flush(); + this->write_array(command, SENSEAIR_REQUEST_LENGTH); + + if (response == nullptr) + return true; + + bool ret = this->read_array(response, SENSEAIR_RESPONSE_LENGTH); + this->flush(); + return ret; +} + +void SenseAirComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SenseAir:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(9600); +} + +} // namespace senseair +} // namespace esphome diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h new file mode 100644 index 0000000000..23bcf40b5a --- /dev/null +++ b/esphome/components/senseair/senseair.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace senseair { + +class SenseAirComponent : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + + void update() override; + void dump_config() override; + + protected: + uint16_t senseair_checksum_(uint8_t *ptr, uint8_t length); + bool senseair_write_command_(const uint8_t *command, uint8_t *response); + + sensor::Sensor *co2_sensor_{nullptr}; +}; + +} // namespace senseair +} // namespace esphome diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py new file mode 100644 index 0000000000..393bfd5182 --- /dev/null +++ b/esphome/components/senseair/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_CO2, CONF_ID, ICON_PERIODIC_TABLE_CO2, UNIT_PARTS_PER_MILLION + +DEPENDENCIES = ['uart'] + +senseair_ns = cg.esphome_ns.namespace('senseair') +SenseAirComponent = senseair_ns.class_('SenseAirComponent', cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SenseAirComponent), + cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), +}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + if CONF_CO2 in config: + sens = yield sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 43f0cefd56..605f72a103 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -6,10 +6,9 @@ from esphome import automation from esphome.components import mqtt from esphome.const import CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, \ CONF_EXPIRE_AFTER, CONF_FILTERS, CONF_FROM, CONF_ICON, CONF_ID, CONF_INTERNAL, \ - CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, \ - CONF_SEND_EVERY, CONF_SEND_FIRST_AT, CONF_TO, CONF_TRIGGER_ID, \ - CONF_UNIT_OF_MEASUREMENT, \ - CONF_WINDOW_SIZE, CONF_NAME, CONF_MQTT_ID + CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, CONF_SEND_EVERY, CONF_SEND_FIRST_AT, \ + CONF_TO, CONF_TRIGGER_ID, CONF_UNIT_OF_MEASUREMENT, CONF_WINDOW_SIZE, CONF_NAME, CONF_MQTT_ID, \ + CONF_FORCE_UPDATE from esphome.core import CORE, coroutine, coroutine_with_priority from esphome.util import Registry @@ -61,6 +60,7 @@ SensorPublishAction = sensor_ns.class_('SensorPublishAction', automation.Action) # Filters Filter = sensor_ns.class_('Filter') +MedianFilter = sensor_ns.class_('MedianFilter', Filter) SlidingWindowMovingAverageFilter = sensor_ns.class_('SlidingWindowMovingAverageFilter', Filter) ExponentialMovingAverageFilter = sensor_ns.class_('ExponentialMovingAverageFilter', Filter) LambdaFilter = sensor_ns.class_('LambdaFilter', Filter) @@ -73,6 +73,7 @@ HeartbeatFilter = sensor_ns.class_('HeartbeatFilter', Filter, cg.Component) DeltaFilter = sensor_ns.class_('DeltaFilter', Filter) OrFilter = sensor_ns.class_('OrFilter', Filter) CalibrateLinearFilter = sensor_ns.class_('CalibrateLinearFilter', Filter) +CalibratePolynomialFilter = sensor_ns.class_('CalibratePolynomialFilter', Filter) SensorInRangeCondition = sensor_ns.class_('SensorInRangeCondition', Filter) unit_of_measurement = cv.string_strict @@ -85,6 +86,7 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({ cv.Optional(CONF_UNIT_OF_MEASUREMENT): unit_of_measurement, cv.Optional(CONF_ICON): icon, cv.Optional(CONF_ACCURACY_DECIMALS): accuracy_decimals, + cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, cv.Optional(CONF_EXPIRE_AFTER): cv.All(cv.requires_component('mqtt'), cv.Any(None, cv.positive_time_period_milliseconds)), cv.Optional(CONF_FILTERS): validate_filters, @@ -126,6 +128,19 @@ def filter_out_filter_to_code(config, filter_id): yield cg.new_Pvariable(filter_id, config) +MEDIAN_SCHEMA = cv.All(cv.Schema({ + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, +}), validate_send_first_at) + + +@FILTER_REGISTRY.register('median', MedianFilter, MEDIAN_SCHEMA) +def median_filter_to_code(config, filter_id): + yield cg.new_Pvariable(filter_id, config[CONF_WINDOW_SIZE], config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT]) + + SLIDING_AVERAGE_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int, cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int, @@ -185,8 +200,15 @@ def debounce_filter_to_code(config, filter_id): yield var +def validate_not_all_from_same(config): + if all(conf[CONF_FROM] == config[0][CONF_FROM] for conf in config): + raise cv.Invalid("The 'from' values of the calibrate_linear filter cannot all point " + "to the same value! Please add more values to the filter.") + return config + + @FILTER_REGISTRY.register('calibrate_linear', CalibrateLinearFilter, cv.All( - cv.ensure_list(validate_datapoint), cv.Length(min=2))) + cv.ensure_list(validate_datapoint), cv.Length(min=2), validate_not_all_from_same)) def calibrate_linear_filter_to_code(config, filter_id): x = [conf[CONF_FROM] for conf in config] y = [conf[CONF_TO] for conf in config] @@ -194,6 +216,32 @@ def calibrate_linear_filter_to_code(config, filter_id): yield cg.new_Pvariable(filter_id, k, b) +CONF_DATAPOINTS = 'datapoints' +CONF_DEGREE = 'degree' + + +def validate_calibrate_polynomial(config): + if config[CONF_DEGREE] >= len(config[CONF_DATAPOINTS]): + raise cv.Invalid("Degree is too high! Maximum possible degree with given datapoints is " + "{}".format(len(config[CONF_DATAPOINTS]) - 1), [CONF_DEGREE]) + return config + + +@FILTER_REGISTRY.register('calibrate_polynomial', CalibratePolynomialFilter, cv.All(cv.Schema({ + cv.Required(CONF_DATAPOINTS): cv.All(cv.ensure_list(validate_datapoint), cv.Length(min=1)), + cv.Required(CONF_DEGREE): cv.positive_int, +}), validate_calibrate_polynomial)) +def calibrate_polynomial_filter_to_code(config, filter_id): + x = [conf[CONF_FROM] for conf in config[CONF_DATAPOINTS]] + y = [conf[CONF_TO] for conf in config[CONF_DATAPOINTS]] + degree = config[CONF_DEGREE] + a = [[1] + [x_**(i+1) for i in range(degree)] for x_ in x] + # Column vector + b = [[v] for v in y] + res = [v[0] for v in _lstsq(a, b)] + yield cg.new_Pvariable(filter_id, res) + + @coroutine def build_filters(config): yield cg.build_registry_list(FILTER_REGISTRY, config) @@ -210,7 +258,8 @@ def setup_sensor_core_(var, config): cg.add(var.set_icon(config[CONF_ICON])) if CONF_ACCURACY_DECIMALS in config: cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) - if CONF_FILTERS in config: + cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) + if config.get(CONF_FILTERS): # must exist and not be empty filters = yield build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) @@ -303,6 +352,66 @@ def fit_linear(x, y): return k, b +def _mat_copy(m): + return [list(row) for row in m] + + +def _mat_transpose(m): + return _mat_copy(zip(*m)) + + +def _mat_identity(n): + return [[int(i == j) for j in range(n)] for i in range(n)] + + +def _mat_dot(a, b): + b_t = _mat_transpose(b) + return [[sum(x*y for x, y in zip(row_a, col_b)) for col_b in b_t] for row_a in a] + + +def _mat_inverse(m): + n = len(m) + m = _mat_copy(m) + id = _mat_identity(n) + + for diag in range(n): + # If diag element is 0, swap rows + if m[diag][diag] == 0: + for i in range(diag+1, n): + if m[i][diag] != 0: + break + else: + raise ValueError("Singular matrix, inverse cannot be calculated!") + + # Swap rows + m[diag], m[i] = m[i], m[diag] + id[diag], id[i] = id[i], id[diag] + + # Scale row to 1 in diagonal + scaler = 1.0 / m[diag][diag] + for j in range(n): + m[diag][j] *= scaler + id[diag][j] *= scaler + + # Subtract diag row + for i in range(n): + if i == diag: + continue + scaler = m[i][diag] + for j in range(n): + m[i][j] -= scaler * m[diag][j] + id[i][j] -= scaler * id[diag][j] + + return id + + +def _lstsq(a, b): + # min_x ||b - ax||^2_2 => x = (a^T a)^{-1} a^T b + a_t = _mat_transpose(a) + x = _mat_inverse(_mat_dot(a_t, a)) + return _mat_dot(_mat_dot(x, a_t), b) + + @coroutine_with_priority(40.0) def to_code(config): cg.add_define('USE_SENSOR') diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 306607dfda..f7a5b5d7ad 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -39,6 +39,44 @@ uint32_t Filter::calculate_remaining_interval(uint32_t input) { } } +// MedianFilter +MedianFilter::MedianFilter(size_t window_size, size_t send_every, size_t send_first_at) + : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} +void MedianFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } +void MedianFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } +optional<float> MedianFilter::new_value(float value) { + if (!isnan(value)) { + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); + } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f)", this, value); + } + + if (++this->send_at_ >= this->send_every_) { + this->send_at_ = 0; + + float median = 0.0f; + if (!this->queue_.empty()) { + std::deque<float> median_queue = this->queue_; + sort(median_queue.begin(), median_queue.end()); + + size_t queue_size = median_queue.size(); + if (queue_size % 2) { + median = median_queue[queue_size / 2]; + } else { + median = (median_queue[queue_size / 2] + median_queue[(queue_size / 2) - 1]) / 2.0f; + } + } + + ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f) SENDING", this, median); + return median; + } + return {}; +} + +uint32_t MedianFilter::expected_interval(uint32_t input) { return input * this->send_every_; } + // SlidingWindowMovingAverageFilter SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every, size_t send_first_at) @@ -128,7 +166,11 @@ optional<float> FilterOutValueFilter::new_value(float value) { else return value; } else { - if (value == this->value_to_filter_out_) + int8_t accuracy = this->parent_->get_accuracy_decimals(); + float accuracy_mult = pow10f(accuracy); + float rounded_filter_out = roundf(accuracy_mult * this->value_to_filter_out_); + float rounded_value = roundf(accuracy_mult * value); + if (rounded_filter_out == rounded_value) return {}; else return value; @@ -228,5 +270,15 @@ float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDW optional<float> CalibrateLinearFilter::new_value(float value) { return value * this->slope_ + this->bias_; } CalibrateLinearFilter::CalibrateLinearFilter(float slope, float bias) : slope_(slope), bias_(bias) {} +optional<float> CalibratePolynomialFilter::new_value(float value) { + float res = 0.0f; + float x = 1.0f; + for (float coefficient : this->coefficients_) { + res += x * coefficient; + x *= value; + } + return res; +} + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 6bd22be230..4c61d4c0a2 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -46,6 +46,36 @@ class Filter { Sensor *parent_{nullptr}; }; +/** Simple median filter. + * + * Takes the median of the last <send_every> values and pushes it out every <send_every>. + */ +class MedianFilter : public Filter { + public: + /** Construct a MedianFilter. + * + * @param window_size The number of values that should be used in median calculation. + * @param send_every After how many sensor values should a new one be pushed out. + * @param send_first_at After how many values to forward the very first value. Defaults to the first value + * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to + * send_every. + */ + explicit MedianFilter(size_t window_size, size_t send_every, size_t send_first_at); + + optional<float> new_value(float value) override; + + void set_send_every(size_t send_every); + void set_window_size(size_t window_size); + + uint32_t expected_interval(uint32_t input) override; + + protected: + std::deque<float> queue_; + size_t send_every_; + size_t send_at_; + size_t window_size_; +}; + /** Simple sliding window moving average filter. * * Essentially just takes takes the average of the last window_size values and pushes them out @@ -243,5 +273,14 @@ class CalibrateLinearFilter : public Filter { float bias_; }; +class CalibratePolynomialFilter : public Filter { + public: + CalibratePolynomialFilter(const std::vector<float> &coefficients) : coefficients_(coefficients) {} + optional<float> new_value(float value) override; + + protected: + std::vector<float> coefficients_; +}; + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 1c7c854394..f23f022767 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -9,14 +9,17 @@ namespace sensor { #define LOG_SENSOR(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ - ESP_LOGCONFIG(TAG, prefix " Unit of Measurement: '%s'", obj->get_unit_of_measurement().c_str()); \ - ESP_LOGCONFIG(TAG, prefix " Accuracy Decimals: %d", obj->get_accuracy_decimals()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, obj->get_unit_of_measurement().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Accuracy Decimals: %d", prefix, obj->get_accuracy_decimals()); \ if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Icon: '%s'", obj->get_icon().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ } \ if (!obj->unique_id().empty()) { \ - ESP_LOGV(TAG, prefix " Unique ID: '%s'", obj->unique_id().c_str()); \ + ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, obj->unique_id().c_str()); \ + } \ + if (obj->get_force_update()) { \ + ESP_LOGV(TAG, "%s Force Update: YES", prefix); \ } \ } @@ -142,6 +145,15 @@ class Sensor : public Nameable { void internal_send_state_to_frontend(float state); + bool get_force_update() const { return force_update_; } + /** Set this sensor's force_update mode. + * + * If the sensor is in force_update mode, the frontend is required to save all + * state changes to the database when they are published, even if the state is the + * same as before. + */ + void set_force_update(bool force_update) { force_update_ = force_update; } + protected: /** Override this to set the Home Assistant unit of measurement for this sensor. * @@ -174,6 +186,7 @@ class Sensor : public Nameable { optional<int8_t> accuracy_decimals_; Filter *filter_list_{nullptr}; ///< Store all active filters. bool has_state_{false}; + bool force_update_{false}; }; class PollingSensorComponent : public PollingComponent, public Sensor { diff --git a/esphome/components/sgp30/__init__.py b/esphome/components/sgp30/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py new file mode 100644 index 0000000000..6329b122fd --- /dev/null +++ b/esphome/components/sgp30/sensor.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ + UNIT_PARTS_PER_BILLION, ICON_PERIODIC_TABLE_CO2 + +DEPENDENCIES = ['i2c'] + +sgp30_ns = cg.esphome_ns.namespace('sgp30') +SGP30Component = sgp30_ns.class_('SGP30Component', cg.PollingComponent, i2c.I2CDevice) + +CONF_ECO2 = 'eco2' +CONF_TVOC = 'tvoc' +CONF_BASELINE = 'baseline' +CONF_UPTIME = 'uptime' +CONF_COMPENSATION = 'compensation' +CONF_HUMIDITY_SOURCE = 'humidity_source' +CONF_TEMPERATURE_SOURCE = 'temperature_source' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SGP30Component), + cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, + ICON_PERIODIC_TABLE_CO2, 0), + cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), + cv.Optional(CONF_BASELINE): cv.hex_uint16_t, + cv.Optional(CONF_COMPENSATION): cv.Schema({ + cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor) + }), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x58)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_ECO2 in config: + sens = yield sensor.new_sensor(config[CONF_ECO2]) + cg.add(var.set_eco2_sensor(sens)) + + if CONF_TVOC in config: + sens = yield sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc_sensor(sens)) + + if CONF_BASELINE in config: + cg.add(var.set_baseline(config[CONF_BASELINE])) + + if CONF_COMPENSATION in config: + compensation_config = config[CONF_COMPENSATION] + sens = yield cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) + cg.add(var.set_humidity_sensor(sens)) + sens = yield cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) + cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp new file mode 100644 index 0000000000..9a73295447 --- /dev/null +++ b/esphome/components/sgp30/sgp30.cpp @@ -0,0 +1,295 @@ +#include "sgp30.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sgp30 { + +static const char *TAG = "sgp30"; + +static const uint16_t SGP30_CMD_GET_SERIAL_ID = 0x3682; +static const uint16_t SGP30_CMD_GET_FEATURESET = 0x202f; +static const uint16_t SGP30_CMD_IAQ_INIT = 0x2003; +static const uint16_t SGP30_CMD_MEASURE_IAQ = 0x2008; +static const uint16_t SGP30_CMD_SET_ABSOLUTE_HUMIDITY = 0x2061; +static const uint16_t SGP30_CMD_GET_IAQ_BASELINE = 0x2015; +static const uint16_t SGP30_CMD_SET_IAQ_BASELINE = 0x201E; + +// Sensor baseline should first be relied on after 1H of operation, +// if the sensor starts with a baseline value provided +const long IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED = 3600; + +// Sensor baseline could first be relied on after 12H of operation, +// if the sensor starts without any prior baseline value provided +const long IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE = 43200; + +void SGP30Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SGP30..."); + + // Serial Number identification + if (!this->write_command_(SGP30_CMD_GET_SERIAL_ID)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + uint16_t raw_serial_number[3]; + + if (!this->read_data_(raw_serial_number, 3)) { + this->mark_failed(); + return; + } + this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | + (uint64_t(raw_serial_number[2])); + ESP_LOGD(TAG, "Serial Number: %llu", this->serial_number_); + + // Featureset identification for future use + if (!this->write_command_(SGP30_CMD_GET_FEATURESET)) { + this->mark_failed(); + return; + } + uint16_t raw_featureset[1]; + if (!this->read_data_(raw_featureset, 1)) { + this->mark_failed(); + return; + } + this->featureset_ = raw_featureset[0]; + if (uint16_t(this->featureset_ >> 12) != 0x0) { + if (uint16_t(this->featureset_ >> 12) == 0x1) { + // ID matching a different sensor: SGPC3 + this->error_code_ = UNSUPPORTED_ID; + } else { + // Unknown ID + this->error_code_ = INVALID_ID; + } + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); + + // Sensor initialization + if (!this->write_command_(SGP30_CMD_IAQ_INIT)) { + ESP_LOGE(TAG, "Sensor sgp30_iaq_init failed."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + + // Sensor baseline reliability timer + if (this->baseline_ > 0) { + this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED; + this->write_iaq_baseline_(this->baseline_); + } else { + this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE; + } +} + +bool SGP30Component::is_sensor_baseline_reliable_() { + if ((this->required_warm_up_time_ == 0) || (std::floor(millis() / 1000) >= this->required_warm_up_time_)) { + // requirement for warm up is removed once the millis uptime surpasses the required warm_up_time + // this avoids the repetitive warm up when the millis uptime is rolled over every ~40 days + this->required_warm_up_time_ = 0; + return true; + } + return false; +} + +void SGP30Component::read_iaq_baseline_() { + if (this->is_sensor_baseline_reliable_()) { + if (!this->write_command_(SGP30_CMD_GET_IAQ_BASELINE)) { + ESP_LOGD(TAG, "Error getting baseline"); + this->status_set_warning(); + return; + } + this->set_timeout(50, [this]() { + uint16_t raw_data[2]; + if (!this->read_data_(raw_data, 2)) { + this->status_set_warning(); + return; + } + + uint8_t eco2baseline = (raw_data[0]); + uint8_t tvocbaseline = (raw_data[1]); + + ESP_LOGI(TAG, "Current eCO2 & TVOC baseline: 0x%04X", uint16_t((eco2baseline << 8) | (tvocbaseline & 0xFF))); + this->status_clear_warning(); + }); + } else { + ESP_LOGD(TAG, "Baseline reading not available for: %.0fs", + (this->required_warm_up_time_ - std::floor(millis() / 1000))); + } +} + +void SGP30Component::send_env_data_() { + if (this->humidity_sensor_ == nullptr && this->temperature_sensor_ == nullptr) + return; + float humidity = NAN; + if (this->humidity_sensor_ != nullptr) + humidity = this->humidity_sensor_->state; + if (isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data."); + return; + } else { + ESP_LOGD(TAG, "External compensation data received: Humidity %0.2f%%", humidity); + } + float temperature = NAN; + if (this->temperature_sensor_ != nullptr) { + temperature = float(this->temperature_sensor_->state); + } + if (isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value data."); + return; + } else { + ESP_LOGD(TAG, "External compensation data received: Temperature %0.2f°C", temperature); + } + + float absolute_humidity = + 216.7f * (((humidity / 100) * 6.112f * std::exp((17.62f * temperature) / (243.12f + temperature))) / + (273.15f + temperature)); + uint8_t humidity_full = uint8_t(std::floor(absolute_humidity)); + uint8_t humidity_dec = uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256)); + ESP_LOGD(TAG, "Calculated Absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity, + uint16_t(uint16_t(humidity_full) << 8 | uint16_t(humidity_dec))); + uint8_t crc = sht_crc_(humidity_full, humidity_dec); + uint8_t data[4]; + data[0] = SGP30_CMD_SET_ABSOLUTE_HUMIDITY & 0xFF; + data[1] = humidity_full; + data[2] = humidity_dec; + data[3] = crc; + if (!this->write_bytes(SGP30_CMD_SET_ABSOLUTE_HUMIDITY >> 8, data, 4)) { + ESP_LOGE(TAG, "Error sending compensation data."); + } +} + +void SGP30Component::write_iaq_baseline_(uint16_t baseline) { + uint8_t e_c_o2_baseline = baseline >> 8; + uint8_t tvoc_baseline = baseline & 0xFF; + uint8_t data[4]; + data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; + data[1] = e_c_o2_baseline; + data[2] = tvoc_baseline; + data[3] = sht_crc_(e_c_o2_baseline, tvoc_baseline); + if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 4)) { + ESP_LOGE(TAG, "Error applying baseline: 0x%04X", baseline); + } else + ESP_LOGI(TAG, "Initial baseline 0x%04X applied successfully!", baseline); +} + +void SGP30Component::dump_config() { + ESP_LOGCONFIG(TAG, "SGP30:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement Initialization failed!"); + break; + case INVALID_ID: + ESP_LOGW(TAG, "Sensor reported an invalid ID. Is this an SGP30?"); + break; + case UNSUPPORTED_ID: + ESP_LOGW(TAG, "Sensor reported an unsupported ID (SGPC3)."); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } else { + ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_); + ESP_LOGCONFIG(TAG, " Baseline: 0x%04X%s", this->baseline_, + ((this->baseline_ != 0x0000) ? " (enabled)" : " (disabled)")); + ESP_LOGCONFIG(TAG, " Warm up time: %lds", this->required_warm_up_time_); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "eCO2", this->eco2_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " Compensation:"); + LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); + } else { + ESP_LOGCONFIG(TAG, " Compensation: No source configured"); + } +} + +void SGP30Component::update() { + if (!this->write_command_(SGP30_CMD_MEASURE_IAQ)) { + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[2]; + if (!this->read_data_(raw_data, 2)) { + this->status_set_warning(); + return; + } + + float eco2 = (raw_data[0]); + float tvoc = (raw_data[1]); + + ESP_LOGD(TAG, "Got eCO2=%.1fppm TVOC=%.1fppb", eco2, tvoc); + if (this->eco2_sensor_ != nullptr) + this->eco2_sensor_->publish_state(eco2); + if (this->tvoc_sensor_ != nullptr) + this->tvoc_sensor_->publish_state(tvoc); + this->status_clear_warning(); + this->send_env_data_(); + this->read_iaq_baseline_(); + }); +} + +bool SGP30Component::write_command_(uint16_t command) { + // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. + return this->write_byte(command >> 8, command & 0xFF); +} + +uint8_t SGP30Component::sht_crc_(uint8_t data1, uint8_t data2) { + uint8_t bit; + uint8_t crc = 0xFF; + + crc ^= data1; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + crc ^= data2; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + return crc; +} + +bool SGP30Component::read_data_(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + return false; + } + + for (uint8_t i = 0; i < len; i++) { + const uint8_t j = 3 * i; + uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + if (crc != buf[j + 2]) { + ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +} // namespace sgp30 +} // namespace esphome diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h new file mode 100644 index 0000000000..2362d1bca6 --- /dev/null +++ b/esphome/components/sgp30/sgp30.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include <cmath> + +namespace esphome { +namespace sgp30 { + +/// This class implements support for the Sensirion SGP30 i2c GAS (VOC and CO2eq) sensors. +class SGP30Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; } + void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + void set_baseline(uint16_t baseline) { baseline_ = baseline; } + void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + void send_env_data_(); + void read_iaq_baseline_(); + bool is_sensor_baseline_reliable_(); + void write_iaq_baseline_(uint16_t baseline); + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + uint64_t serial_number_; + uint16_t featureset_; + long required_warm_up_time_; + + enum ErrorCode { + COMMUNICATION_FAILED, + MEASUREMENT_INIT_FAILED, + INVALID_ID, + UNSUPPORTED_ID, + UNKNOWN + } error_code_{UNKNOWN}; + + sensor::Sensor *eco2_sensor_{nullptr}; + sensor::Sensor *tvoc_sensor_{nullptr}; + uint16_t baseline_{0x0000}; + /// Input sensor for humidity and temperature compensation. + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; +}; + +} // namespace sgp30 +} // namespace esphome diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index f23c0d59b4..559fdc21ab 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -43,8 +43,14 @@ void SHT3XDComponent::dump_config() { } float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA; } void SHT3XDComponent::update() { - if (!this->write_command_(SHT3XD_COMMAND_POLLING_H)) + if (this->status_has_warning()) { + ESP_LOGD(TAG, "Retrying to reconnect the sensor."); + this->write_command_(SHT3XD_COMMAND_SOFT_RESET); + } + if (!this->write_command_(SHT3XD_COMMAND_POLLING_H)) { + this->status_set_warning(); return; + } this->set_timeout(50, [this]() { uint16_t raw_data[2]; diff --git a/esphome/components/shtcx/__init__.py b/esphome/components/shtcx/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py new file mode 100644 index 0000000000..eb215078e7 --- /dev/null +++ b/esphome/components/shtcx/sensor.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, ICON_WATER_PERCENT, \ + ICON_THERMOMETER, UNIT_CELSIUS, UNIT_PERCENT + +DEPENDENCIES = ['i2c'] + +shtcx_ns = cg.esphome_ns.namespace('shtcx') +SHTCXComponent = shtcx_ns.class_('SHTCXComponent', cg.PollingComponent, i2c.I2CDevice) + +SHTCXType = shtcx_ns.enum('SHTCXType') + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SHTCXComponent), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x70)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp new file mode 100644 index 0000000000..b8daceb1af --- /dev/null +++ b/esphome/components/shtcx/shtcx.cpp @@ -0,0 +1,166 @@ +#include "shtcx.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace shtcx { + +static const char *TAG = "shtcx"; + +static const uint16_t SHTCX_COMMAND_SLEEP = 0xB098; +static const uint16_t SHTCX_COMMAND_WAKEUP = 0x3517; +static const uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8; +static const uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D; +static const uint16_t SHTCX_COMMAND_POLLING_H = 0x7866; + +inline const char *to_string(SHTCXType type) { + switch (type) { + case SHTCX_TYPE_SHTC3: + return "SHTC3"; + case SHTCX_TYPE_SHTC1: + return "SHTC1"; + default: + return "[Unknown model]"; + } +} + +void SHTCXComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up SHTCx..."); + this->soft_reset(); + + if (!this->write_command_(SHTCX_COMMAND_READ_ID_REGISTER)) { + ESP_LOGE(TAG, "Error requesting Device ID"); + this->mark_failed(); + return; + } + + uint16_t device_id_register[1]; + if (!this->read_data_(device_id_register, 1)) { + ESP_LOGE(TAG, "Error reading Device ID"); + this->mark_failed(); + return; + } + + if (((device_id_register[0] << 2) & 0x1C) == 0x1C) { + if ((device_id_register[0] & 0x847) == 0x847) { + this->type_ = SHTCX_TYPE_SHTC3; + } else { + this->type_ = SHTCX_TYPE_SHTC1; + } + } else { + this->type_ = SHTCX_TYPE_UNKNOWN; + } + ESP_LOGCONFIG(TAG, " Device identified: %s", to_string(this->type_)); +} +void SHTCXComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SHTCx:"); + ESP_LOGCONFIG(TAG, " Model: %s", to_string(this->type_)); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with SHTCx failed!"); + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} +float SHTCXComponent::get_setup_priority() const { return setup_priority::DATA; } +void SHTCXComponent::update() { + if (this->status_has_warning()) { + ESP_LOGW(TAG, "Retrying to reconnect the sensor."); + this->soft_reset(); + } + if (this->type_ != SHTCX_TYPE_SHTC1) { + this->wake_up(); + } + if (!this->write_command_(SHTCX_COMMAND_POLLING_H)) { + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[2]; + if (!this->read_data_(raw_data, 2)) { + this->status_set_warning(); + return; + } + + float temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f; + float humidity = 100.0f * float(raw_data[1]) / 65536.0f; + + ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + this->status_clear_warning(); + if (this->type_ != SHTCX_TYPE_SHTC1) { + this->sleep(); + } + }); +} + +bool SHTCXComponent::write_command_(uint16_t command) { + // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. + return this->write_byte(command >> 8, command & 0xFF); +} + +uint8_t sht_crc(uint8_t data1, uint8_t data2) { + uint8_t bit; + uint8_t crc = 0xFF; + + crc ^= data1; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + crc ^= data2; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + return crc; +} + +bool SHTCXComponent::read_data_(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + return false; + } + + for (uint8_t i = 0; i < len; i++) { + const uint8_t j = 3 * i; + uint8_t crc = sht_crc(buf[j], buf[j + 1]); + if (crc != buf[j + 2]) { + ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +void SHTCXComponent::soft_reset() { + this->write_command_(SHTCX_COMMAND_SOFT_RESET); + delayMicroseconds(200); +} +void SHTCXComponent::sleep() { this->write_command_(SHTCX_COMMAND_SLEEP); } + +void SHTCXComponent::wake_up() { + this->write_command_(SHTCX_COMMAND_WAKEUP); + delayMicroseconds(200); +} + +} // namespace shtcx +} // namespace esphome diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h new file mode 100644 index 0000000000..ccc6533bfa --- /dev/null +++ b/esphome/components/shtcx/shtcx.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace shtcx { + +enum SHTCXType { SHTCX_TYPE_SHTC3 = 0, SHTCX_TYPE_SHTC1, SHTCX_TYPE_UNKNOWN }; + +/// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors. +class SHTCXComponent : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void soft_reset(); + void sleep(); + void wake_up(); + + protected: + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + SHTCXType type_; + sensor::Sensor *temperature_sensor_; + sensor::Sensor *humidity_sensor_; +}; + +} // namespace shtcx +} // namespace esphome diff --git a/esphome/components/shutdown/shutdown_switch.cpp b/esphome/components/shutdown/shutdown_switch.cpp index d27bb8aadc..ce33cd187f 100644 --- a/esphome/components/shutdown/shutdown_switch.cpp +++ b/esphome/components/shutdown/shutdown_switch.cpp @@ -14,7 +14,7 @@ void ShutdownSwitch::write_state(bool state) { if (state) { ESP_LOGI(TAG, "Shutting down..."); - delay(100); // Let MQTT settle a bit + delay(100); // NOLINT App.run_safe_shutdown_hooks(); #ifdef ARDUINO_ARCH_ESP8266 diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py new file mode 100644 index 0000000000..c64112570a --- /dev/null +++ b/esphome/components/sim800l/__init__.py @@ -0,0 +1,59 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.components import uart + +DEPENDENCIES = ['uart'] + +sim800l_ns = cg.esphome_ns.namespace('sim800l') +Sim800LComponent = sim800l_ns.class_('Sim800LComponent', cg.Component) + +Sim800LReceivedMessageTrigger = sim800l_ns.class_('Sim800LReceivedMessageTrigger', + automation.Trigger.template(cg.std_string, + cg.std_string)) + +# Actions +Sim800LSendSmsAction = sim800l_ns.class_('Sim800LSendSmsAction', automation.Action) + +MULTI_CONF = True + +CONF_ON_SMS_RECEIVED = 'on_sms_received' +CONF_RECIPIENT = 'recipient' +CONF_MESSAGE = 'message' + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(Sim800LComponent), + cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Sim800LReceivedMessageTrigger), + }), +}).extend(cv.polling_component_schema('5s')).extend(uart.UART_DEVICE_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + for conf in config.get(CONF_ON_SMS_RECEIVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [(cg.std_string, 'message'), + (cg.std_string, 'sender')], conf) + + +SIM800L_SEND_SMS_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(Sim800LComponent), + cv.Required(CONF_RECIPIENT): cv.templatable(cv.string_strict), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), +}) + + +@automation.register_action('sim800l.send_sms', Sim800LSendSmsAction, SIM800L_SEND_SMS_SCHEMA) +def sim800l_send_sms_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_RECIPIENT], args, cg.std_string) + cg.add(var.set_recipient(template_)) + template_ = yield cg.templatable(config[CONF_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + yield var diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp new file mode 100644 index 0000000000..1390ef8b49 --- /dev/null +++ b/esphome/components/sim800l/sim800l.cpp @@ -0,0 +1,264 @@ +#include "sim800l.h" +#include "esphome/core/log.h" +#include <string.h> + +namespace esphome { +namespace sim800l { + +static const char* TAG = "sim800l"; + +const char ASCII_CR = 0x0D; +const char ASCII_LF = 0x0A; + +void Sim800LComponent::update() { + if (this->watch_dog_++ == 2) { + this->state_ = STATE_INIT; + this->write(26); + } + + if (state_ == STATE_INIT) { + if (this->registered_ && this->send_pending_) { + this->send_cmd_("AT+CSCS=\"GSM\""); + this->state_ = STATE_SENDINGSMS1; + } else { + this->send_cmd_("AT"); + this->state_ = STATE_CHECK_AT; + } + this->expect_ack_ = true; + } + if (state_ == STATE_RECEIVEDSMS) { + // Serial Buffer should have flushed. + // Send cmd to delete received sms + char delete_cmd[20]; + sprintf(delete_cmd, "AT+CMGD=%d", this->parse_index_); + this->send_cmd_(delete_cmd); + this->state_ = STATE_CHECK_SMS; + this->expect_ack_ = true; + } +} + +void Sim800LComponent::send_cmd_(std::string message) { + ESP_LOGV(TAG, "S: %s - %d", message.c_str(), this->state_); + this->watch_dog_ = 0; + this->write_str(message.c_str()); + this->write_byte(ASCII_LF); +} + +void Sim800LComponent::parse_cmd_(std::string message) { + ESP_LOGV(TAG, "R: %s - %d", message.c_str(), this->state_); + + if (message.empty()) + return; + + if (this->expect_ack_) { + bool ok = message == "OK"; + this->expect_ack_ = false; + if (!ok) { + if (this->state_ == STATE_CHECK_AT && message == "AT") { + // Expected ack but AT echo received + this->state_ = STATE_DISABLE_ECHO; + this->expect_ack_ = true; + } else { + ESP_LOGW(TAG, "Not ack. %d %s", this->state_, message.c_str()); + this->state_ = STATE_IDLE; // Let it timeout + return; + } + } + } + + switch (this->state_) { + case STATE_INIT: { + // While we were waiting for update to check for messages, this notifies a message + // is available. + bool message_available = message.compare(0, 6, "+CMTI:") == 0; + if (!message_available) + break; + // Else fall thru ... + } + case STATE_CHECK_SMS: + send_cmd_("AT+CMGL=\"ALL\""); + this->state_ = STATE_PARSE_SMS; + this->parse_index_ = 0; + break; + case STATE_DISABLE_ECHO: + send_cmd_("ATE0"); + this->state_ = STATE_CHECK_AT; + this->expect_ack_ = true; + break; + case STATE_CHECK_AT: + send_cmd_("AT+CMGF=1"); + this->state_ = STATE_CREG; + this->expect_ack_ = true; + break; + case STATE_CREG: + send_cmd_("AT+CREG?"); + this->state_ = STATE_CREGWAIT; + break; + case STATE_CREGWAIT: { + // Response: "+CREG: 0,1" -- the one there means registered ok + // "+CREG: -,-" means not registered ok + bool registered = message.compare(0, 6, "+CREG:") == 0 && message[9] == '1'; + if (registered) { + if (!this->registered_) + ESP_LOGD(TAG, "Registered OK"); + this->state_ = STATE_CSQ; + this->expect_ack_ = true; + } else { + ESP_LOGW(TAG, "Registration Fail"); + if (message[7] == '0') { // Network registration is disable, enable it + send_cmd_("AT+CREG=1"); + this->expect_ack_ = true; + this->state_ = STATE_CHECK_AT; + } else { + // Keep waiting registration + this->state_ = STATE_CREG; + } + } + this->registered_ = registered; + break; + } + case STATE_CSQ: + send_cmd_("AT+CSQ"); + this->state_ = STATE_CSQ_RESPONSE; + break; + case STATE_CSQ_RESPONSE: + if (message.compare(0, 5, "+CSQ:") == 0) { + size_t comma = message.find(',', 6); + if (comma != 6) { + this->rssi_ = strtol(message.substr(6, comma - 6).c_str(), nullptr, 10); + ESP_LOGD(TAG, "RSSI: %d", this->rssi_); + } + } + this->expect_ack_ = true; + this->state_ = STATE_CHECK_SMS; + break; + case STATE_PARSE_SMS: + this->state_ = STATE_PARSE_SMS_RESPONSE; + break; + case STATE_PARSE_SMS_RESPONSE: + if (message.compare(0, 6, "+CMGL:") == 0 && this->parse_index_ == 0) { + size_t start = 7; + size_t end = message.find(',', start); + uint8_t item = 0; + while (end != start) { + item++; + if (item == 1) { // Slot Index + this->parse_index_ = strtol(message.substr(start, end - start).c_str(), nullptr, 10); + } + // item 2 = STATUS, usually "REC UNERAD" + if (item == 3) { // recipient + // Add 1 and remove 2 from substring to get rid of "quotes" + this->sender_ = message.substr(start + 1, end - start - 2); + break; + } + // item 4 = "" + // item 5 = Received timestamp + start = end + 1; + end = message.find(',', start); + } + + if (item < 2) { + ESP_LOGD(TAG, "Invalid message %d %s", this->state_, message.c_str()); + return; + } + this->state_ = STATE_RECEIVESMS; + } + // Otherwise we receive another OK, we do nothing just wait polling to continuously check for SMS + if (message == "OK") + this->state_ = STATE_INIT; + break; + case STATE_RECEIVESMS: + /* Our recipient is set and the message body is in message + kick ESPHome callback now + */ + ESP_LOGD(TAG, "Received SMS from: %s", this->sender_.c_str()); + ESP_LOGD(TAG, "%s", message.c_str()); + this->callback_.call(message, this->sender_); + /* If the message is multiline, next lines will contain message data. + If there were other messages in the list, next line will be +CMGL: ... + At the end of the list the new line and the OK should be received. + To keep this simple just first line of message if considered, then + the next state will swallow all received data and in next poll event + this message index is marked for deletion. + */ + this->state_ = STATE_RECEIVEDSMS; + break; + case STATE_RECEIVEDSMS: + // Let the buffer flush. Next poll will request to delete the parsed index message. + break; + case STATE_SENDINGSMS1: + this->send_cmd_("AT+CMGS=\"" + this->recipient_ + "\""); + this->state_ = STATE_SENDINGSMS2; + break; + case STATE_SENDINGSMS2: + if (message == ">") { + // Send sms body + ESP_LOGD(TAG, "Sending message: '%s'", this->outgoing_message_.c_str()); + this->write_str(this->outgoing_message_.c_str()); + this->write(26); + this->state_ = STATE_SENDINGSMS3; + } else { + this->registered_ = false; + this->state_ = STATE_INIT; + this->send_cmd_("AT+CMEE=2"); + this->write(26); + } + break; + case STATE_SENDINGSMS3: + if (message.compare(0, 6, "+CMGS:") == 0) { + ESP_LOGD(TAG, "SMS Sent OK: %s", message.c_str()); + this->send_pending_ = false; + this->state_ = STATE_CHECK_SMS; + this->expect_ack_ = true; + } + break; + default: + ESP_LOGD(TAG, "Unhandled: %s - %d", message.c_str(), this->state_); + break; + } +} + +void Sim800LComponent::loop() { + // Read message + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == SIM800L_READ_BUFFER_LENGTH) + this->read_pos_ = 0; + + ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte); // NOLINT + + if (byte == ASCII_CR) + continue; + if (byte >= 0x7F) + byte = '?'; // need to be valid utf8 string for log functions. + this->read_buffer_[this->read_pos_] = byte; + + if (this->state_ == STATE_SENDINGSMS2 && this->read_pos_ == 0 && byte == '>') + this->read_buffer_[++this->read_pos_] = ASCII_LF; + + if (this->read_buffer_[this->read_pos_] == ASCII_LF) { + this->read_buffer_[this->read_pos_] = 0; + this->read_pos_ = 0; + this->parse_cmd_(this->read_buffer_); + } else { + this->read_pos_++; + } + } +} + +void Sim800LComponent::send_sms(std::string recipient, std::string message) { + ESP_LOGD(TAG, "Sending to %s: %s", recipient.c_str(), message.c_str()); + this->recipient_ = recipient; + this->outgoing_message_ = message; + this->send_pending_ = true; + this->update(); +} +void Sim800LComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SIM800L:"); + ESP_LOGCONFIG(TAG, " RSSI: %d dB", this->rssi_); +} + +} // namespace sim800l +} // namespace esphome diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h new file mode 100644 index 0000000000..696eb8890f --- /dev/null +++ b/esphome/components/sim800l/sim800l.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" + +namespace esphome { +namespace sim800l { + +const uint8_t SIM800L_READ_BUFFER_LENGTH = 255; + +enum State { + STATE_IDLE = 0, + STATE_INIT, + STATE_CHECK_AT, + STATE_CREG, + STATE_CREGWAIT, + STATE_CSQ, + STATE_CSQ_RESPONSE, + STATE_IDLEWAIT, + STATE_SENDINGSMS1, + STATE_SENDINGSMS2, + STATE_SENDINGSMS3, + STATE_CHECK_SMS, + STATE_PARSE_SMS, + STATE_PARSE_SMS_RESPONSE, + STATE_RECEIVESMS, + STATE_READSMS, + STATE_RECEIVEDSMS, + STATE_DELETEDSMS, + STATE_DISABLE_ECHO, + STATE_PARSE_SMS_OK +}; + +class Sim800LComponent : public uart::UARTDevice, public PollingComponent { + public: + /// Retrieve the latest sensor values. This operation takes approximately 16ms. + void update() override; + void loop() override; + void dump_config() override; + void add_on_sms_received_callback(std::function<void(std::string, std::string)> callback) { + this->callback_.add(std::move(callback)); + } + void send_sms(std::string recipient, std::string message); + + protected: + void send_cmd_(std::string); + void parse_cmd_(std::string); + + std::string sender_; + char read_buffer_[SIM800L_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + uint8_t parse_index_{0}; + uint8_t watch_dog_{0}; + bool expect_ack_{false}; + sim800l::State state_{STATE_IDLE}; + bool registered_{false}; + int rssi_{0}; + + std::string recipient_; + std::string outgoing_message_; + bool send_pending_; + + CallbackManager<void(std::string, std::string)> callback_; +}; + +class Sim800LReceivedMessageTrigger : public Trigger<std::string, std::string> { + public: + explicit Sim800LReceivedMessageTrigger(Sim800LComponent *parent) { + parent->add_on_sms_received_callback( + [this](std::string message, std::string sender) { this->trigger(message, sender); }); + } +}; + +template<typename... Ts> class Sim800LSendSmsAction : public Action<Ts...> { + public: + Sim800LSendSmsAction(Sim800LComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, recipient) + TEMPLATABLE_VALUE(std::string, message) + + void play(Ts... x) { + auto recipient = this->recipient_.value(x...); + auto message = this->message_.value(x...); + this->parent_->send_sms(recipient, message); + } + + protected: + Sim800LComponent *parent_; +}; + +} // namespace sim800l +} // namespace esphome diff --git a/esphome/components/sm16716/__init__.py b/esphome/components/sm16716/__init__.py new file mode 100644 index 0000000000..4e342588f9 --- /dev/null +++ b/esphome/components/sm16716/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import (CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_ID, + CONF_NUM_CHANNELS, CONF_NUM_CHIPS) + +AUTO_LOAD = ['output'] +sm16716_ns = cg.esphome_ns.namespace('sm16716') +SM16716 = sm16716_ns.class_('SM16716', cg.Component) + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SM16716), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_NUM_CHANNELS, default=3): cv.int_range(min=3, max=255), + cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=85), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + data = yield cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) + + cg.add(var.set_num_channels(config[CONF_NUM_CHANNELS])) + cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) diff --git a/esphome/components/sm16716/output.py b/esphome/components/sm16716/output.py new file mode 100644 index 0000000000..93c9ed4ce1 --- /dev/null +++ b/esphome/components/sm16716/output.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import SM16716 + +DEPENDENCIES = ['sm16716'] + +Channel = SM16716.class_('Channel', output.FloatOutput) + +CONF_SM16716_ID = 'sm16716_id' +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ + cv.GenerateID(CONF_SM16716_ID): cv.use_id(SM16716), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield output.register_output(var, config) + + parent = yield cg.get_variable(config[CONF_SM16716_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/sm16716/sm16716.cpp b/esphome/components/sm16716/sm16716.cpp new file mode 100644 index 0000000000..bc8e4fc1f4 --- /dev/null +++ b/esphome/components/sm16716/sm16716.cpp @@ -0,0 +1,52 @@ +#include "sm16716.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sm16716 { + +static const char *TAG = "sm16716"; + +void SM16716::setup() { + ESP_LOGCONFIG(TAG, "Setting up SM16716OutputComponent..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(false); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(false); + this->pwm_amounts_.resize(this->num_channels_, 0); +} +void SM16716::dump_config() { + ESP_LOGCONFIG(TAG, "SM16716:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); + ESP_LOGCONFIG(TAG, " Total number of channels: %u", this->num_channels_); + ESP_LOGCONFIG(TAG, " Number of chips: %u", this->num_chips_); +} +void SM16716::loop() { + if (!this->update_) + return; + + for (uint8_t i = 0; i < 50; i++) { + this->write_bit_(false); + } + + // send 25 bits (1 start bit plus 24 data bits) for each chip + for (uint8_t index = 0; index < this->num_channels_; index++) { + // send a start bit initially and after every 3 channels + if (index % 3 == 0) { + this->write_bit_(true); + } + + this->write_byte_(this->pwm_amounts_[index]); + } + + // send a blank 25 bits to signal the end + this->write_bit_(false); + this->write_byte_(0); + this->write_byte_(0); + this->write_byte_(0); + + this->update_ = false; +} + +} // namespace sm16716 +} // namespace esphome diff --git a/esphome/components/sm16716/sm16716.h b/esphome/components/sm16716/sm16716.h new file mode 100644 index 0000000000..85f78c8cf5 --- /dev/null +++ b/esphome/components/sm16716/sm16716.h @@ -0,0 +1,71 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace sm16716 { + +class SM16716 : public Component { + public: + class Channel; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_num_channels(uint8_t num_channels) { num_channels_ = num_channels; } + void set_num_chips(uint8_t num_chips) { num_chips_ = num_chips; } + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Send new values if they were updated. + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(SM16716 *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + void write_state(float state) override { + auto amount = uint8_t(state * 0xFF); + this->parent_->set_channel_value_(this->channel_, amount); + } + + SM16716 *parent_; + uint8_t channel_; + }; + + protected: + void set_channel_value_(uint8_t channel, uint8_t value) { + uint8_t index = this->num_channels_ - channel - 1; + if (this->pwm_amounts_[index] != value) { + this->update_ = true; + } + this->pwm_amounts_[index] = value; + } + void write_bit_(bool value) { + this->data_pin_->digital_write(value); + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(false); + } + void write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + this->write_bit_(data & mask); + } + } + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t num_channels_; + uint8_t num_chips_; + std::vector<uint8_t> pwm_amounts_; + bool update_{true}; +}; + +} // namespace sm16716 +} // namespace esphome diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 4f931203fb..c10a3e5ac3 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #ifdef ARDUINO_ARCH_ESP32 -#include "apps/sntp/sntp.h" +#include "lwip/apps/sntp.h" #endif #ifdef ARDUINO_ARCH_ESP8266 #include "sntp.h" diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index ae6d09dfac..65ee5960f0 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -29,4 +29,4 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) - cg.add(var.set_oscillation(oscillation_output)) + cg.add(var.set_oscillating(oscillation_output)) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index db4b71c29a..bf2a18955a 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -8,74 +8,10 @@ namespace spi { static const char *TAG = "spi"; -void ICACHE_RAM_ATTR HOT SPIComponent::write_byte(uint8_t data) { - uint8_t send_bits = data; - if (this->msb_first_) - send_bits = reverse_bits_8(data); - - this->clk_->digital_write(true); - if (!this->high_speed_) - delayMicroseconds(5); - - for (size_t i = 0; i < 8; i++) { - if (!this->high_speed_) - delayMicroseconds(5); - this->clk_->digital_write(false); - - // sampling on leading edge - this->mosi_->digital_write(send_bits & (1 << i)); - if (!this->high_speed_) - delayMicroseconds(5); - this->clk_->digital_write(true); - } - - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data); -} - -uint8_t ICACHE_RAM_ATTR HOT SPIComponent::read_byte() { - this->clk_->digital_write(true); - - uint8_t data = 0; - for (size_t i = 0; i < 8; i++) { - if (!this->high_speed_) - delayMicroseconds(5); - data |= uint8_t(this->miso_->digital_read()) << i; - this->clk_->digital_write(false); - if (!this->high_speed_) - delayMicroseconds(5); - this->clk_->digital_write(true); - } - - if (this->msb_first_) { - data = reverse_bits_8(data); - } - - ESP_LOGVV(TAG, " Received 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data); - - return data; -} -void ICACHE_RAM_ATTR HOT SPIComponent::read_array(uint8_t *data, size_t length) { - for (size_t i = 0; i < length; i++) - data[i] = this->read_byte(); -} - -void ICACHE_RAM_ATTR HOT SPIComponent::write_array(uint8_t *data, size_t length) { - for (size_t i = 0; i < length; i++) { - App.feed_wdt(); - this->write_byte(data[i]); - } -} - -void ICACHE_RAM_ATTR HOT SPIComponent::enable(GPIOPin *cs, bool msb_first, bool high_speed) { - ESP_LOGVV(TAG, "Enabling SPI Chip on pin %u...", cs->get_pin()); - cs->digital_write(false); - - this->active_cs_ = cs; - this->msb_first_ = msb_first; - this->high_speed_ = high_speed; -} - void ICACHE_RAM_ATTR HOT SPIComponent::disable() { + if (this->hw_spi_ != nullptr) { + this->hw_spi_->endTransaction(); + } ESP_LOGVV(TAG, "Disabling SPI Chip on pin %u...", this->active_cs_->get_pin()); this->active_cs_->digital_write(true); this->active_cs_ = nullptr; @@ -84,6 +20,53 @@ void SPIComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI bus..."); this->clk_->setup(); this->clk_->digital_write(true); + + bool use_hw_spi = true; + if (this->clk_->is_inverted()) + use_hw_spi = false; + const bool has_miso = this->miso_ != nullptr; + const bool has_mosi = this->mosi_ != nullptr; + if (has_miso && this->miso_->is_inverted()) + use_hw_spi = false; + if (has_mosi && this->mosi_->is_inverted()) + use_hw_spi = false; + int8_t clk_pin = this->clk_->get_pin(); + int8_t miso_pin = has_miso ? this->miso_->get_pin() : -1; + int8_t mosi_pin = has_mosi ? this->mosi_->get_pin() : -1; +#ifdef ARDUINO_ARCH_ESP8266 + if (clk_pin == 6 && miso_pin == 7 && mosi_pin == 8) { + // pass + } else if (clk_pin == 14 && miso_pin == 12 && mosi_pin == 13) { + // pass + } else { + use_hw_spi = false; + } + + if (use_hw_spi) { + this->hw_spi_ = &SPI; + this->hw_spi_->pins(clk_pin, miso_pin, mosi_pin, 0); + this->hw_spi_->begin(); + return; + } +#endif +#ifdef ARDUINO_ARCH_ESP32 + static uint8_t spi_bus_num = 0; + if (spi_bus_num >= 2) { + use_hw_spi = false; + } + + if (use_hw_spi) { + if (spi_bus_num == 0) { + this->hw_spi_ = &SPI; + } else { + this->hw_spi_ = new SPIClass(VSPI); + } + spi_bus_num++; + this->hw_spi_->begin(clk_pin, miso_pin, mosi_pin); + return; + } +#endif + if (this->miso_ != nullptr) { this->miso_->setup(); } @@ -97,8 +80,154 @@ void SPIComponent::dump_config() { LOG_PIN(" CLK Pin: ", this->clk_); LOG_PIN(" MISO Pin: ", this->miso_); LOG_PIN(" MOSI Pin: ", this->mosi_); + ESP_LOGCONFIG(TAG, " Using HW SPI: %s", YESNO(this->hw_spi_ != nullptr)); } float SPIComponent::get_setup_priority() const { return setup_priority::BUS; } +void SPIComponent::debug_tx(uint8_t value) { + ESP_LOGVV(TAG, " TX 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(value), value); +} +void SPIComponent::debug_rx(uint8_t value) { + ESP_LOGVV(TAG, " RX 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(value), value); +} +void SPIComponent::debug_enable(uint8_t pin) { ESP_LOGVV(TAG, "Enabling SPI Chip on pin %u...", pin); } + +void SPIComponent::cycle_clock_(bool value) { + uint32_t start = ESP.getCycleCount(); + while (start - ESP.getCycleCount() < this->wait_cycle_) + ; + this->clk_->digital_write(value); + start += this->wait_cycle_; + while (start - ESP.getCycleCount() < this->wait_cycle_) + ; +} + +// NOLINTNEXTLINE +#pragma GCC optimize("unroll-loops") +// NOLINTNEXTLINE +#pragma GCC optimize("O2") + +template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, bool READ, bool WRITE> +uint8_t HOT SPIComponent::transfer_(uint8_t data) { + // Clock starts out at idle level + this->clk_->digital_write(CLOCK_POLARITY); + uint8_t out_data = 0; + + for (uint8_t i = 0; i < 8; i++) { + uint8_t shift; + if (BIT_ORDER == BIT_ORDER_MSB_FIRST) + shift = 7 - i; + else + shift = i; + + if (CLOCK_PHASE == CLOCK_PHASE_LEADING) { + // sampling on leading edge + if (WRITE) { + this->mosi_->digital_write(data & (1 << shift)); + } + + // SAMPLE! + this->cycle_clock_(!CLOCK_POLARITY); + + if (READ) { + out_data |= uint8_t(this->miso_->digital_read()) << shift; + } + + this->cycle_clock_(CLOCK_POLARITY); + } else { + // sampling on trailing edge + this->cycle_clock_(!CLOCK_POLARITY); + + if (WRITE) { + this->mosi_->digital_write(data & (1 << shift)); + } + + // SAMPLE! + this->cycle_clock_(CLOCK_POLARITY); + + if (READ) { + out_data |= uint8_t(this->miso_->digital_read()) << shift; + } + } + } + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + if (WRITE) { + SPIComponent::debug_tx(data); + } + if (READ) { + SPIComponent::debug_rx(out_data); + } +#endif + + App.feed_wdt(); + + return out_data; +} + +// Generate with (py3): +// +// from itertools import product +// bit_orders = ['BIT_ORDER_LSB_FIRST', 'BIT_ORDER_MSB_FIRST'] +// clock_pols = ['CLOCK_POLARITY_LOW', 'CLOCK_POLARITY_HIGH'] +// clock_phases = ['CLOCK_PHASE_LEADING', 'CLOCK_PHASE_TRAILING'] +// reads = [False, True] +// writes = [False, True] +// cpp_bool = {False: 'false', True: 'true'} +// for b, cpol, cph, r, w in product(bit_orders, clock_pols, clock_phases, reads, writes): +// if not r and not w: +// continue +// print(f"template uint8_t SPIComponent::transfer_<{b}, {cpol}, {cph}, {cpp_bool[r]}, {cpp_bool[w]}>(uint8_t +// data);") + +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, false, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, false>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, false, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, false>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, false, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, false>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, false, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, false>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_LSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, false, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, false>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_LEADING, true, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, false, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, false>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_LOW, CLOCK_PHASE_TRAILING, true, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, false, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, false>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_LEADING, true, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, false, true>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, false>( + uint8_t data); +template uint8_t SPIComponent::transfer_<BIT_ORDER_MSB_FIRST, CLOCK_POLARITY_HIGH, CLOCK_PHASE_TRAILING, true, true>( + uint8_t data); + } // namespace spi } // namespace esphome diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 600e1a0cd2..ccef6192f3 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -2,10 +2,61 @@ #include "esphome/core/component.h" #include "esphome/core/esphal.h" +#include <SPI.h> namespace esphome { namespace spi { +/// The bit-order for SPI devices. This defines how the data read from and written to the device is interpreted. +enum SPIBitOrder { + /// The least significant bit is transmitted/received first. + BIT_ORDER_LSB_FIRST, + /// The most significant bit is transmitted/received first. + BIT_ORDER_MSB_FIRST, +}; +/** The SPI clock signal polarity, + * + * This defines how the clock signal is used. Flipping this effectively inverts the clock signal. + */ +enum SPIClockPolarity { + /** The clock signal idles on LOW. (CPOL=0) + * + * A rising edge means a leading edge for the clock. + */ + CLOCK_POLARITY_LOW = false, + /** The clock signal idles on HIGH. (CPOL=1) + * + * A falling edge means a trailing edge for the clock. + */ + CLOCK_POLARITY_HIGH = true, +}; +/** The SPI clock signal phase. + * + * This defines when the data signals are sampled. Most SPI devices use the LEADING clock phase. + */ +enum SPIClockPhase { + /// The data is sampled on a leading clock edge. (CPHA=0) + CLOCK_PHASE_LEADING, + /// The data is sampled on a trailing clock edge. (CPHA=1) + CLOCK_PHASE_TRAILING, +}; +/** The SPI clock signal data rate. This defines for what duration the clock signal is HIGH/LOW. + * So effectively the rate of bytes can be calculated using + * + * effective_byte_rate = spi_data_rate / 16 + * + * Implementations can use the pre-defined constants here, or use an integer in the template definition + * to manually use a specific data rate. + */ +enum SPIDataRate : uint32_t { + DATA_RATE_1KHZ = 1000, + DATA_RATE_200KHZ = 200000, + DATA_RATE_1MHZ = 1000000, + DATA_RATE_2MHZ = 2000000, + DATA_RATE_4MHZ = 4000000, + DATA_RATE_8MHZ = 8000000, +}; + class SPIComponent : public Component { public: void set_clk(GPIOPin *clk) { clk_ = clk; } @@ -16,59 +67,156 @@ class SPIComponent : public Component { void dump_config() override; - uint8_t read_byte(); + template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> uint8_t read_byte() { + if (this->hw_spi_ != nullptr) { + return this->hw_spi_->transfer(0x00); + } + return this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, false>(0x00); + } - void read_array(uint8_t *data, size_t length); + template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> + void read_array(uint8_t *data, size_t length) { + if (this->hw_spi_ != nullptr) { + this->hw_spi_->transfer(data, length); + return; + } + for (size_t i = 0; i < length; i++) { + data[i] = this->read_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(); + } + } - void write_byte(uint8_t data); + template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> + void write_byte(uint8_t data) { + if (this->hw_spi_ != nullptr) { + this->hw_spi_->write(data); + return; + } + this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, false, true>(data); + } - void write_array(uint8_t *data, size_t length); + template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> + void write_array(const uint8_t *data, size_t length) { + if (this->hw_spi_ != nullptr) { + auto *data_c = const_cast<uint8_t *>(data); + this->hw_spi_->writeBytes(data_c, length); + return; + } + for (size_t i = 0; i < length; i++) { + this->write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data[i]); + } + } - void enable(GPIOPin *cs, bool msb_first, bool high_speed); + template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> + uint8_t transfer_byte(uint8_t data) { + if (this->hw_spi_ != nullptr) { + return this->hw_spi_->transfer(data); + } + return this->transfer_<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, true, true>(data); + } + + template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE> + void transfer_array(uint8_t *data, size_t length) { + if (this->hw_spi_ != nullptr) { + this->hw_spi_->transfer(data, length); + return; + } + for (size_t i = 0; i < length; i++) { + data[i] = this->transfer_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data[i]); + } + } + + template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, uint32_t DATA_RATE> + void enable(GPIOPin *cs) { + SPIComponent::debug_enable(cs->get_pin()); + + if (this->hw_spi_ != nullptr) { + uint8_t data_mode = (uint8_t(CLOCK_POLARITY) << 1) | uint8_t(CLOCK_PHASE); + SPISettings settings(DATA_RATE, BIT_ORDER, data_mode); + this->hw_spi_->beginTransaction(settings); + } else { + this->clk_->digital_write(CLOCK_POLARITY); + this->wait_cycle_ = uint32_t(F_CPU) / DATA_RATE / 2ULL; + } + + this->active_cs_ = cs; + this->active_cs_->digital_write(false); + } void disable(); float get_setup_priority() const override; protected: + inline void cycle_clock_(bool value); + + static void debug_enable(uint8_t pin); + static void debug_tx(uint8_t value); + static void debug_rx(uint8_t value); + + template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, bool READ, bool WRITE> + uint8_t transfer_(uint8_t data); + GPIOPin *clk_; GPIOPin *miso_{nullptr}; GPIOPin *mosi_{nullptr}; GPIOPin *active_cs_{nullptr}; - bool msb_first_{true}; - bool high_speed_{false}; + SPIClass *hw_spi_{nullptr}; + uint32_t wait_cycle_; }; +template<SPIBitOrder BIT_ORDER, SPIClockPolarity CLOCK_POLARITY, SPIClockPhase CLOCK_PHASE, SPIDataRate DATA_RATE> class SPIDevice { public: SPIDevice() = default; SPIDevice(SPIComponent *parent, GPIOPin *cs) : parent_(parent), cs_(cs) {} - void set_spi_parent(SPIComponent *parent) { this->parent_ = parent; } - void set_cs_pin(GPIOPin *cs) { this->cs_ = cs; } + void set_spi_parent(SPIComponent *parent) { parent_ = parent; } + void set_cs_pin(GPIOPin *cs) { cs_ = cs; } void spi_setup() { this->cs_->setup(); this->cs_->digital_write(true); } - void enable() { this->parent_->enable(this->cs_, this->is_device_msb_first(), this->is_device_high_speed()); } + void enable() { this->parent_->template enable<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE, DATA_RATE>(this->cs_); } void disable() { this->parent_->disable(); } - uint8_t read_byte() { return this->parent_->read_byte(); } + uint8_t read_byte() { return this->parent_->template read_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(); } - void read_array(uint8_t *data, size_t length) { return this->parent_->read_array(data, length); } + void read_array(uint8_t *data, size_t length) { + return this->parent_->template read_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length); + } - void write_byte(uint8_t data) { return this->parent_->write_byte(data); } + template<size_t N> std::array<uint8_t, N> read_array() { + std::array<uint8_t, N> data; + this->read_array(data.data(), N); + return data; + } - void write_array(uint8_t *data, size_t length) { this->parent_->write_array(data, length); } + void write_byte(uint8_t data) { + return this->parent_->template write_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data); + } + + void write_array(const uint8_t *data, size_t length) { + this->parent_->template write_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length); + } + + template<size_t N> void write_array(const std::array<uint8_t, N> &data) { this->write_array(data.data(), N); } + + void write_array(const std::vector<uint8_t> &data) { this->write_array(data.data(), data.size()); } + + uint8_t transfer_byte(uint8_t data) { + return this->parent_->template transfer_byte<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data); + } + + void transfer_array(uint8_t *data, size_t length) { + this->parent_->template transfer_array<BIT_ORDER, CLOCK_POLARITY, CLOCK_PHASE>(data, length); + } + + template<size_t N> void transfer_array(std::array<uint8_t, N> &data) { this->transfer_array(data.data(), N); } protected: - virtual bool is_device_msb_first() = 0; - - virtual bool is_device_high_speed() { return false; } - SPIComponent *parent_{nullptr}; GPIOPin *cs_{nullptr}; }; diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 0a678452b2..047ddddcac 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import display -from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN +from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN, \ + CONF_BRIGHTNESS from esphome.core import coroutine ssd1306_base_ns = cg.esphome_ns.namespace('ssd1306_base') @@ -25,6 +26,7 @@ SSD1306_MODEL = cv.enum(MODELS, upper=True, space="_") SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ cv.Required(CONF_MODEL): SSD1306_MODEL, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, }).extend(cv.polling_component_schema('1s')) @@ -38,6 +40,8 @@ def setup_ssd1036(var, config): if CONF_RESET_PIN in config: reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) if CONF_EXTERNAL_VCC in config: cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index b6f2d94eac..d60f7dc985 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -79,10 +79,7 @@ void SSD1306::setup() { case SH1106_MODEL_128_64: case SSD1306_MODEL_64_48: case SH1106_MODEL_64_48: - if (this->external_vcc_) - this->command(0x9F); - else - this->command(0xCF); + this->command(int(255 * (this->brightness_))); break; case SSD1306_MODEL_96_16: case SH1106_MODEL_96_16: @@ -100,7 +97,7 @@ void SSD1306::setup() { this->command(0xF1); this->command(SSD1306_COMMAND_SET_VCOM_DETECT); - this->command(0x40); + this->command(0x00); this->command(SSD1306_COMMAND_DISPLAY_ALL_ON_RESUME); this->command(SSD1306_NORMAL_DISPLAY); diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 66c12ec938..8adf3c1b87 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -29,6 +29,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void set_model(SSD1306Model model) { this->model_ = model; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } + void set_brightness(float brightness) { this->brightness_ = brightness; } float get_setup_priority() const override { return setup_priority::PROCESSOR; } void fill(int color) override; @@ -50,6 +51,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { SSD1306Model model_{SSD1306_MODEL_128_64}; GPIOPin *reset_pin_{nullptr}; bool external_vcc_{false}; + float brightness_{1.0}; }; } // namespace ssd1306_base diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index aeead612ff..d87f412f70 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -7,7 +7,6 @@ namespace ssd1306_spi { static const char *TAG = "ssd1306_spi"; -bool SPISSD1306::is_device_msb_first() { return true; } void SPISSD1306::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI SSD1306..."); this->spi_setup(); @@ -52,7 +51,6 @@ void HOT SPISSD1306::write_display_data() { this->disable(); } } -bool SPISSD1306::is_device_high_speed() { return true; } } // namespace ssd1306_spi } // namespace esphome diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.h b/esphome/components/ssd1306_spi/ssd1306_spi.h index 5d0640bd84..c58ebc800a 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.h +++ b/esphome/components/ssd1306_spi/ssd1306_spi.h @@ -7,7 +7,9 @@ namespace esphome { namespace ssd1306_spi { -class SPISSD1306 : public ssd1306_base::SSD1306, public spi::SPIDevice { +class SPISSD1306 : public ssd1306_base::SSD1306, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, + spi::DATA_RATE_8MHZ> { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } @@ -19,8 +21,6 @@ class SPISSD1306 : public ssd1306_base::SSD1306, public spi::SPIDevice { void command(uint8_t value) override; void write_display_data() override; - bool is_device_msb_first() override; - bool is_device_high_speed() override; GPIOPin *dc_pin_; }; diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py new file mode 100644 index 0000000000..69e11ec0d1 --- /dev/null +++ b/esphome/components/ssd1325_base/__init__.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN +from esphome.core import coroutine + +ssd1325_base_ns = cg.esphome_ns.namespace('ssd1325_base') +SSD1325 = ssd1325_base_ns.class_('SSD1325', cg.PollingComponent, display.DisplayBuffer) +SSD1325Model = ssd1325_base_ns.enum('SSD1325Model') + +MODELS = { + 'SSD1325_128X32': SSD1325Model.SSD1325_MODEL_128_32, + 'SSD1325_128X64': SSD1325Model.SSD1325_MODEL_128_64, + 'SSD1325_96X16': SSD1325Model.SSD1325_MODEL_96_16, + 'SSD1325_64X48': SSD1325Model.SSD1325_MODEL_64_48, +} + +SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.Required(CONF_MODEL): SSD1325_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, +}).extend(cv.polling_component_schema('1s')) + + +@coroutine +def setup_ssd1036(var, config): + yield cg.register_component(var, config) + yield display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_EXTERNAL_VCC in config: + cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp new file mode 100644 index 0000000000..3079e19cc8 --- /dev/null +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -0,0 +1,177 @@ +#include "ssd1325_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1325_base { + +static const char *TAG = "ssd1325"; + +static const uint8_t BLACK = 0; +static const uint8_t WHITE = 1; + +static const uint8_t SSD1325_SETCOLADDR = 0x15; +static const uint8_t SSD1325_SETROWADDR = 0x75; +static const uint8_t SSD1325_SETCONTRAST = 0x81; +static const uint8_t SSD1325_SETCURRENT = 0x84; + +static const uint8_t SSD1325_SETREMAP = 0xA0; +static const uint8_t SSD1325_SETSTARTLINE = 0xA1; +static const uint8_t SSD1325_SETOFFSET = 0xA2; +static const uint8_t SSD1325_NORMALDISPLAY = 0xA4; +static const uint8_t SSD1325_DISPLAYALLON = 0xA5; +static const uint8_t SSD1325_DISPLAYALLOFF = 0xA6; +static const uint8_t SSD1325_INVERTDISPLAY = 0xA7; +static const uint8_t SSD1325_SETMULTIPLEX = 0xA8; +static const uint8_t SSD1325_MASTERCONFIG = 0xAD; +static const uint8_t SSD1325_DISPLAYOFF = 0xAE; +static const uint8_t SSD1325_DISPLAYON = 0xAF; + +static const uint8_t SSD1325_SETPRECHARGECOMPENABLE = 0xB0; +static const uint8_t SSD1325_SETPHASELEN = 0xB1; +static const uint8_t SSD1325_SETROWPERIOD = 0xB2; +static const uint8_t SSD1325_SETCLOCK = 0xB3; +static const uint8_t SSD1325_SETPRECHARGECOMP = 0xB4; +static const uint8_t SSD1325_SETGRAYTABLE = 0xB8; +static const uint8_t SSD1325_SETPRECHARGEVOLTAGE = 0xBC; +static const uint8_t SSD1325_SETVCOMLEVEL = 0xBE; +static const uint8_t SSD1325_SETVSL = 0xBF; + +static const uint8_t SSD1325_GFXACCEL = 0x23; +static const uint8_t SSD1325_DRAWRECT = 0x24; +static const uint8_t SSD1325_COPY = 0x25; + +void SSD1325::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1325_DISPLAYOFF); /* display off */ + this->command(SSD1325_SETCLOCK); /* set osc division */ + this->command(0xF1); /* 145 */ + this->command(SSD1325_SETMULTIPLEX); /* multiplex ratio */ + this->command(0x3f); /* duty = 1/64 */ + this->command(SSD1325_SETOFFSET); /* set display offset --- */ + this->command(0x4C); /* 76 */ + this->command(SSD1325_SETSTARTLINE); /*set start line */ + this->command(0x00); /* ------ */ + this->command(SSD1325_MASTERCONFIG); /*Set Master Config DC/DC Converter*/ + this->command(0x02); + this->command(SSD1325_SETREMAP); /* set segment remap------ */ + this->command(0x56); + this->command(SSD1325_SETCURRENT + 0x2); /* Set Full Current Range */ + this->command(SSD1325_SETGRAYTABLE); + this->command(0x01); + this->command(0x11); + this->command(0x22); + this->command(0x32); + this->command(0x43); + this->command(0x54); + this->command(0x65); + this->command(0x76); + this->command(SSD1325_SETCONTRAST); /* set contrast current */ + this->command(0x7F); // max! + this->command(SSD1325_SETROWPERIOD); + this->command(0x51); + this->command(SSD1325_SETPHASELEN); + this->command(0x55); + this->command(SSD1325_SETPRECHARGECOMP); + this->command(0x02); + this->command(SSD1325_SETPRECHARGECOMPENABLE); + this->command(0x28); + this->command(SSD1325_SETVCOMLEVEL); // Set High Voltage Level of COM Pin + this->command(0x1C); //? + this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin + this->command(0x0D | 0x02); + this->command(SSD1325_NORMALDISPLAY); /* set display mode */ + this->command(SSD1325_DISPLAYON); /* display ON */ +} +void SSD1325::display() { + this->command(SSD1325_SETCOLADDR); /* set column address */ + this->command(0x00); /* set column start address */ + this->command(0x3F); /* set column end address */ + this->command(SSD1325_SETROWADDR); /* set row address */ + this->command(0x00); /* set row start address */ + this->command(0x3F); /* set row end address */ + + this->write_display_data(); +} +void SSD1325::update() { + this->do_update_(); + this->display(); +} +int SSD1325::get_height_internal() { + switch (this->model_) { + case SSD1325_MODEL_128_32: + return 32; + case SSD1325_MODEL_128_64: + return 64; + case SSD1325_MODEL_96_16: + return 16; + case SSD1325_MODEL_64_48: + return 48; + default: + return 0; + } +} +int SSD1325::get_width_internal() { + switch (this->model_) { + case SSD1325_MODEL_128_32: + case SSD1325_MODEL_128_64: + return 128; + case SSD1325_MODEL_96_16: + return 96; + case SSD1325_MODEL_64_48: + return 64; + default: + return 0; + } +} +size_t SSD1325::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; +} + +void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, int color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + uint16_t pos = x + (y / 8) * this->get_width_internal(); + uint8_t subpos = y % 8; + if (color) { + this->buffer_[pos] |= (1 << subpos); + } else { + this->buffer_[pos] &= ~(1 << subpos); + } +} +void SSD1325::fill(int color) { + uint8_t fill = color ? 0xFF : 0x00; + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->buffer_[i] = fill; +} +void SSD1325::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1325::model_str_() { + switch (this->model_) { + case SSD1325_MODEL_128_32: + return "SSD1325 128x32"; + case SSD1325_MODEL_128_64: + return "SSD1325 128x64"; + case SSD1325_MODEL_96_16: + return "SSD1325 96x16"; + case SSD1325_MODEL_64_48: + return "SSD1325 64x48"; + default: + return "Unknown"; + } +} + +} // namespace ssd1325_base +} // namespace esphome diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h new file mode 100644 index 0000000000..e227f68f86 --- /dev/null +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1325_base { + +enum SSD1325Model { + SSD1325_MODEL_128_32 = 0, + SSD1325_MODEL_128_64, + SSD1325_MODEL_96_16, + SSD1325_MODEL_64_48, +}; + +class SSD1325 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1325Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(int color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + void draw_absolute_pixel_internal(int x, int y, int color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + const char *model_str_(); + + SSD1325Model model_{SSD1325_MODEL_128_64}; + GPIOPin *reset_pin_{nullptr}; + bool external_vcc_{false}; +}; + +} // namespace ssd1325_base +} // namespace esphome diff --git a/esphome/components/ssd1325_spi/__init__.py b/esphome/components/ssd1325_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py new file mode 100644 index 0000000000..4615d45393 --- /dev/null +++ b/esphome/components/ssd1325_spi/display.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1325_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +AUTO_LOAD = ['ssd1325_base'] +DEPENDENCIES = ['spi'] + +ssd1325_spi = cg.esphome_ns.namespace('ssd1325_spi') +SPISSD1325 = ssd1325_spi.class_('SPISSD1325', ssd1325_base.SSD1325, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(ssd1325_base.SSD1325_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPISSD1325), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield ssd1325_base.setup_ssd1036(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp new file mode 100644 index 0000000000..399700f1dd --- /dev/null +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -0,0 +1,64 @@ +#include "ssd1325_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1325_spi { + +static const char *TAG = "ssd1325_spi"; + +void SPISSD1325::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1325..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1325::setup(); +} +void SPISSD1325::dump_config() { + LOG_DISPLAY("", "SPI SSD1325", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1325::command(uint8_t value) { + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + this->cs_->digital_write(false); + this->write_byte(value); + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1325::write_display_data() { + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + this->cs_->digital_write(false); + delay(1); + this->enable(); + for (uint16_t x = 0; x < this->get_width_internal(); x += 2) { + for (uint16_t y = 0; y < this->get_height_internal(); y += 8) { // we write 8 pixels at once + uint8_t left8 = this->buffer_[y * 16 + x]; + uint8_t right8 = this->buffer_[y * 16 + x + 1]; + for (uint8_t p = 0; p < 8; p++) { + uint8_t d = 0; + if (left8 & (1 << p)) + d |= 0xF0; + if (right8 & (1 << p)) + d |= 0x0F; + this->write_byte(d); + } + } + } + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1325_spi +} // namespace esphome diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.h b/esphome/components/ssd1325_spi/ssd1325_spi.h new file mode 100644 index 0000000000..e4e7d55769 --- /dev/null +++ b/esphome/components/ssd1325_spi/ssd1325_spi.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1325_base/ssd1325_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1325_spi { + +class SPISSD1325 : public ssd1325_base::SSD1325, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING, + spi::DATA_RATE_8MHZ> { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1325_spi +} // namespace esphome diff --git a/esphome/components/status/status_binary_sensor.cpp b/esphome/components/status/status_binary_sensor.cpp index 7fbeb8c171..90ac1faad7 100644 --- a/esphome/components/status/status_binary_sensor.cpp +++ b/esphome/components/status/status_binary_sensor.cpp @@ -30,7 +30,7 @@ void StatusBinarySensor::loop() { this->publish_state(status); } -void StatusBinarySensor::setup() { this->publish_state(false); } +void StatusBinarySensor::setup() { this->publish_initial_state(false); } void StatusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Status Binary Sensor", this); } } // namespace status diff --git a/esphome/components/sts3x/__init__.py b/esphome/components/sts3x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py new file mode 100644 index 0000000000..f48deeeae5 --- /dev/null +++ b/esphome/components/sts3x/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, ICON_THERMOMETER, UNIT_CELSIUS + +DEPENDENCIES = ['i2c'] + +sts3x_ns = cg.esphome_ns.namespace('sts3x') + +STS3XComponent = sts3x_ns.class_('STS3XComponent', sensor.Sensor, + cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ + cv.GenerateID(): cv.declare_id(STS3XComponent), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x4A)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp new file mode 100644 index 0000000000..1a24a17caf --- /dev/null +++ b/esphome/components/sts3x/sts3x.cpp @@ -0,0 +1,123 @@ +#include "sts3x.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sts3x { + +static const char *TAG = "sts3x"; + +static const uint16_t STS3X_COMMAND_READ_SERIAL_NUMBER = 0x3780; +static const uint16_t STS3X_COMMAND_READ_STATUS = 0xF32D; +static const uint16_t STS3X_COMMAND_SOFT_RESET = 0x30A2; +static const uint16_t STS3X_COMMAND_POLLING_H = 0x2400; + +/// Commands for future use +static const uint16_t STS3X_COMMAND_CLEAR_STATUS = 0x3041; +static const uint16_t STS3X_COMMAND_HEATER_ENABLE = 0x306D; +static const uint16_t STS3X_COMMAND_HEATER_DISABLE = 0x3066; +static const uint16_t STS3X_COMMAND_FETCH_DATA = 0xE000; + +void STS3XComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up STS3x..."); + if (!this->write_command_(STS3X_COMMAND_READ_SERIAL_NUMBER)) { + this->mark_failed(); + return; + } + + uint16_t raw_serial_number[2]; + if (!this->read_data_(raw_serial_number, 1)) { + this->mark_failed(); + return; + } + uint32_t serial_number = (uint32_t(raw_serial_number[0]) << 16); + ESP_LOGV(TAG, " Serial Number: 0x%08X", serial_number); +} +void STS3XComponent::dump_config() { + ESP_LOGCONFIG(TAG, "STS3x:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with ST3x failed!"); + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "STS3x", this); +} +float STS3XComponent::get_setup_priority() const { return setup_priority::DATA; } +void STS3XComponent::update() { + if (this->status_has_warning()) { + ESP_LOGD(TAG, "Retrying to reconnect the sensor."); + this->write_command_(STS3X_COMMAND_SOFT_RESET); + } + if (!this->write_command_(STS3X_COMMAND_POLLING_H)) { + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[1]; + if (!this->read_data_(raw_data, 1)) { + this->status_set_warning(); + return; + } + + float temperature = 175.0f * float(raw_data[0]) / 65535.0f - 45.0f; + ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature); + this->publish_state(temperature); + this->status_clear_warning(); + }); +} + +bool STS3XComponent::write_command_(uint16_t command) { + // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. + return this->write_byte(command >> 8, command & 0xFF); +} + +uint8_t sts3x_crc(uint8_t data1, uint8_t data2) { + uint8_t bit; + uint8_t crc = 0xFF; + + crc ^= data1; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + crc ^= data2; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + return crc; +} + +bool STS3XComponent::read_data_(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + return false; + } + + for (uint8_t i = 0; i < len; i++) { + const uint8_t j = 3 * i; + uint8_t crc = sts3x_crc(buf[j], buf[j + 1]); + if (crc != buf[j + 2]) { + ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +} // namespace sts3x +} // namespace esphome diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h new file mode 100644 index 0000000000..436cf938d8 --- /dev/null +++ b/esphome/components/sts3x/sts3x.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sts3x { + +/// This class implements support for the ST3x-DIS family of temperature i2c sensors. +class STS3XComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); +}; + +} // namespace sts3x +} // namespace esphome diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index 0bcdedcabd..fef0902181 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import time from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID +from esphome.py_compat import string_types sun_ns = cg.esphome_ns.namespace('sun') @@ -31,9 +32,9 @@ ELEVATION_MAP = { def elevation(value): - if isinstance(value, str): + if isinstance(value, string_types): try: - value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')] + value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')(value)] except cv.Invalid: pass value = cv.angle(value) diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index be4fc24c4a..cd6cec429f 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -9,15 +9,15 @@ namespace switch_ { #define LOG_SWITCH(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Icon: '%s'", obj->get_icon().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ } \ if (obj->assumed_state()) { \ - ESP_LOGCONFIG(TAG, prefix " Assumed State: YES"); \ + ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ if (obj->is_inverted()) { \ - ESP_LOGCONFIG(TAG, prefix " Inverted: YES"); \ + ESP_LOGCONFIG(TAG, "%s Inverted: YES", prefix); \ } \ } diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py new file mode 100644 index 0000000000..11fcfe3955 --- /dev/null +++ b/esphome/components/sx1509/__init__.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED + +CONF_KEYPAD = 'keypad' +CONF_KEY_ROWS = 'key_rows' +CONF_KEY_COLUMNS = 'key_columns' +CONF_SLEEP_TIME = 'sleep_time' +CONF_SCAN_TIME = 'scan_time' +CONF_DEBOUNCE_TIME = 'debounce_time' + +DEPENDENCIES = ['i2c'] +MULTI_CONF = True + +sx1509_ns = cg.esphome_ns.namespace('sx1509') +SX1509GPIOMode = sx1509_ns.enum('SX1509GPIOMode') +SX1509_GPIO_MODES = { + 'INPUT': SX1509GPIOMode.SX1509_INPUT, + 'INPUT_PULLUP': SX1509GPIOMode.SX1509_INPUT_PULLUP, + 'OUTPUT': SX1509GPIOMode.SX1509_OUTPUT +} + +SX1509Component = sx1509_ns.class_('SX1509Component', cg.Component, i2c.I2CDevice) +SX1509GPIOPin = sx1509_ns.class_('SX1509GPIOPin', cg.GPIOPin) + +KEYPAD_SCHEMA = cv.Schema({ + cv.Required(CONF_KEY_ROWS): cv.int_range(min=1, max=8), + cv.Required(CONF_KEY_COLUMNS): cv.int_range(min=1, max=8), + cv.Optional(CONF_SLEEP_TIME): cv.int_range(min=128, max=8192), + cv.Optional(CONF_SCAN_TIME): cv.int_range(min=1, max=128), + cv.Optional(CONF_DEBOUNCE_TIME): cv.int_range(min=1, max=64), +}) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SX1509Component), + cv.Optional(CONF_KEYPAD): cv.Schema(KEYPAD_SCHEMA), +}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x3E)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + if CONF_KEYPAD in config: + keypad = config[CONF_KEYPAD] + cg.add(var.set_rows_cols(keypad[CONF_KEY_ROWS], keypad[CONF_KEY_COLUMNS])) + if CONF_SLEEP_TIME in keypad and CONF_SCAN_TIME in keypad and CONF_DEBOUNCE_TIME in keypad: + cg.add(var.set_sleep_time(keypad[CONF_SLEEP_TIME])) + cg.add(var.set_scan_time(keypad[CONF_SCAN_TIME])) + cg.add(var.set_debounce_time(keypad[CONF_DEBOUNCE_TIME])) + + +CONF_SX1509 = 'sx1509' +CONF_SX1509_ID = 'sx1509_id' + +SX1509_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_SX1509): cv.use_id(SX1509Component), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(SX1509_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +SX1509_INPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_SX1509): cv.use_id(SX1509Component), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="INPUT"): cv.enum(SX1509_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_SX1509, + (SX1509_OUTPUT_PIN_SCHEMA, SX1509_INPUT_PIN_SCHEMA)) +def sx1509_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_SX1509]) + yield SX1509GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], + config[CONF_INVERTED]) diff --git a/esphome/components/sx1509/binary_sensor/__init__.py b/esphome/components/sx1509/binary_sensor/__init__.py new file mode 100644 index 0000000000..9a65524383 --- /dev/null +++ b/esphome/components/sx1509/binary_sensor/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ID +from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID + +CONF_ROW = 'row' +CONF_COL = 'col' + +DEPENDENCIES = ['sx1509'] + +SX1509BinarySensor = sx1509_ns.class_('SX1509BinarySensor', binary_sensor.BinarySensor) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SX1509BinarySensor), + cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), + cv.Required(CONF_ROW): cv.int_range(min=0, max=4), + cv.Required(CONF_COL): cv.int_range(min=0, max=4), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield binary_sensor.register_binary_sensor(var, config) + hub = yield cg.get_variable(config[CONF_SX1509_ID]) + cg.add(var.set_row_col(config[CONF_ROW], config[CONF_COL])) + + cg.add(hub.register_keypad_binary_sensor(var)) diff --git a/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h b/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h new file mode 100644 index 0000000000..2eef19782c --- /dev/null +++ b/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/sx1509/sx1509.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace sx1509 { + +class SX1509BinarySensor : public sx1509::SX1509Processor, public binary_sensor::BinarySensor { + public: + void set_row_col(uint8_t row, uint8_t col) { this->key_ = (1 << (col + 8)) | (1 << row); } + void process(uint16_t data) override { this->publish_state(static_cast<bool>(data == key_)); } + + protected: + uint16_t key_{0}; +}; + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/output/__init__.py b/esphome/components/sx1509/output/__init__.py new file mode 100644 index 0000000000..80aec0afd4 --- /dev/null +++ b/esphome/components/sx1509/output/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_PIN, CONF_ID +from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID + +DEPENDENCIES = ['sx1509'] + +SX1509FloatOutputChannel = sx1509_ns.class_('SX1509FloatOutputChannel', + output.FloatOutput, cg.Component) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ + cv.Required(CONF_ID): cv.declare_id(SX1509FloatOutputChannel), + cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), + cv.Required(CONF_PIN): cv.int_range(min=0, max=15), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + parent = yield cg.get_variable(config[CONF_SX1509_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield output.register_output(var, config) + cg.add(var.set_pin(config[CONF_PIN])) + cg.add(var.set_parent(parent)) diff --git a/esphome/components/sx1509/output/sx1509_float_output.cpp b/esphome/components/sx1509/output/sx1509_float_output.cpp new file mode 100644 index 0000000000..7ff1bbb61b --- /dev/null +++ b/esphome/components/sx1509/output/sx1509_float_output.cpp @@ -0,0 +1,30 @@ +#include "sx1509_float_output.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sx1509 { + +static const char *TAG = "sx1509_float_channel"; + +void SX1509FloatOutputChannel::write_state(float state) { + const uint16_t max_duty = 255; + const float duty_rounded = roundf(state * max_duty); + auto duty = static_cast<uint16_t>(duty_rounded); + this->parent_->set_pin_value(this->pin_, duty); +} + +void SX1509FloatOutputChannel::setup() { + ESP_LOGD(TAG, "setup pin %d", this->pin_); + this->parent_->pin_mode(this->pin_, SX1509_ANALOG_OUTPUT); + this->turn_off(); +} + +void SX1509FloatOutputChannel::dump_config() { + ESP_LOGCONFIG(TAG, "SX1509 PWM:"); + ESP_LOGCONFIG(TAG, " sx1509 pin: %d", this->pin_); + LOG_FLOAT_OUTPUT(this); +} + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/output/sx1509_float_output.h b/esphome/components/sx1509/output/sx1509_float_output.h new file mode 100644 index 0000000000..39e51839ea --- /dev/null +++ b/esphome/components/sx1509/output/sx1509_float_output.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/components/sx1509/sx1509.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace sx1509 { + +class SX1509Component; + +class SX1509FloatOutputChannel : public output::FloatOutput, public Component { + public: + void set_parent(SX1509Component *parent) { this->parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void write_state(float state) override; + + SX1509Component *parent_; + uint8_t pin_; +}; + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp new file mode 100644 index 0000000000..2806a1cac2 --- /dev/null +++ b/esphome/components/sx1509/sx1509.cpp @@ -0,0 +1,253 @@ +#include "sx1509.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sx1509 { + +static const char *TAG = "sx1509"; + +void SX1509Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SX1509Component..."); + + ESP_LOGV(TAG, " Resetting devices..."); + if (!this->write_byte(REG_RESET, 0x12)) { + this->mark_failed(); + return; + } + this->write_byte(REG_RESET, 0x34); + + uint16_t data; + this->read_byte_16(REG_INTERRUPT_MASK_A, &data); + if (data == 0xFF00) { + clock_(INTERNAL_CLOCK_2MHZ); + } else { + this->mark_failed(); + return; + } + delayMicroseconds(500); + if (this->has_keypad_) + this->setup_keypad_(); +} + +void SX1509Component::dump_config() { + ESP_LOGCONFIG(TAG, "SX1509:"); + if (this->is_failed()) { + ESP_LOGE(TAG, "Setting up SX1509 failed!"); + } + LOG_I2C_DEVICE(this); +} + +void SX1509Component::loop() { + if (this->has_keypad_) { + uint16_t key_data = this->read_key_data(); + for (auto *binary_sensor : this->keypad_binary_sensors_) + binary_sensor->process(key_data); + } +} + +bool SX1509Component::digital_read(uint8_t pin) { + if (this->ddr_mask_ & (1 << pin)) { + uint16_t temp_reg_data; + this->read_byte_16(REG_DATA_B, &temp_reg_data); + if (temp_reg_data & (1 << pin)) + return true; + } + return false; +} + +void SX1509Component::digital_write(uint8_t pin, bool bit_value) { + if ((~this->ddr_mask_) & (1 << pin)) { + // If the pin is an output, write high/low + uint16_t temp_reg_data = 0; + this->read_byte_16(REG_DATA_B, &temp_reg_data); + if (bit_value) + temp_reg_data |= (1 << pin); + else + temp_reg_data &= ~(1 << pin); + this->write_byte_16(REG_DATA_B, temp_reg_data); + } else { + // Otherwise the pin is an input, pull-up/down + uint16_t temp_pullup; + this->read_byte_16(REG_PULL_UP_B, &temp_pullup); + uint16_t temp_pull_down; + this->read_byte_16(REG_PULL_DOWN_B, &temp_pull_down); + + if (bit_value) { + // if HIGH, do pull-up, disable pull-down + temp_pullup |= (1 << pin); + temp_pull_down &= ~(1 << pin); + this->write_byte_16(REG_PULL_UP_B, temp_pullup); + this->write_byte_16(REG_PULL_DOWN_B, temp_pull_down); + } else { + // If LOW do pull-down, disable pull-up + temp_pull_down |= (1 << pin); + temp_pullup &= ~(1 << pin); + this->write_byte_16(REG_PULL_UP_B, temp_pullup); + this->write_byte_16(REG_PULL_DOWN_B, temp_pull_down); + } + } +} + +void SX1509Component::pin_mode(uint8_t pin, uint8_t mode) { + this->read_byte_16(REG_DIR_B, &this->ddr_mask_); + if ((mode == SX1509_OUTPUT) || (mode == SX1509_ANALOG_OUTPUT)) + this->ddr_mask_ &= ~(1 << pin); + else + this->ddr_mask_ |= (1 << pin); + this->write_byte_16(REG_DIR_B, this->ddr_mask_); + + if (mode == INPUT_PULLUP) + digital_write(pin, HIGH); + + if (mode == SX1509_ANALOG_OUTPUT) { + setup_led_driver_(pin); + } +} + +void SX1509Component::setup_led_driver_(uint8_t pin) { + uint16_t temp_word; + uint8_t temp_byte; + + this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word); + temp_word |= (1 << pin); + this->write_byte_16(REG_INPUT_DISABLE_B, temp_word); + + this->read_byte_16(REG_PULL_UP_B, &temp_word); + temp_word &= ~(1 << pin); + this->write_byte_16(REG_PULL_UP_B, temp_word); + + this->ddr_mask_ &= ~(1 << pin); // 0=output + this->write_byte_16(REG_DIR_B, this->ddr_mask_); + + this->read_byte(REG_CLOCK, &temp_byte); + temp_byte |= (1 << 6); // Internal 2MHz oscillator part 1 (set bit 6) + temp_byte &= ~(1 << 5); // Internal 2MHz oscillator part 2 (clear bit 5) + this->write_byte(REG_CLOCK, temp_byte); + + this->read_byte(REG_MISC, &temp_byte); + temp_byte &= ~(1 << 7); // set linear mode bank B + temp_byte &= ~(1 << 3); // set linear mode bank A + temp_byte |= 0x70; // Frequency of the LED Driver clock ClkX of all IOs: + this->write_byte(REG_MISC, temp_byte); + + this->read_byte_16(REG_LED_DRIVER_ENABLE_B, &temp_word); + temp_word |= (1 << pin); + this->write_byte_16(REG_LED_DRIVER_ENABLE_B, temp_word); + + this->read_byte_16(REG_DATA_B, &temp_word); + temp_word &= ~(1 << pin); + this->write_byte_16(REG_DATA_B, temp_word); +} + +void SX1509Component::clock_(byte osc_source, byte osc_pin_function, byte osc_freq_out, byte osc_divider) { + osc_source = (osc_source & 0b11) << 5; // 2-bit value, bits 6:5 + osc_pin_function = (osc_pin_function & 1) << 4; // 1-bit value bit 4 + osc_freq_out = (osc_freq_out & 0b1111); // 4-bit value, bits 3:0 + byte reg_clock = osc_source | osc_pin_function | osc_freq_out; + this->write_byte(REG_CLOCK, reg_clock); + + osc_divider = constrain(osc_divider, 1, 7); + this->clk_x_ = 2000000; + osc_divider = (osc_divider & 0b111) << 4; // 3-bit value, bits 6:4 + + uint8_t reg_misc; + this->read_byte(REG_MISC, ®_misc); + reg_misc &= ~(0b111 << 4); + reg_misc |= osc_divider; + this->write_byte(REG_MISC, reg_misc); +} + +void SX1509Component::setup_keypad_() { + uint8_t temp_byte; + + // setup row/col pins for INPUT OUTPUT + this->read_byte_16(REG_DIR_B, &this->ddr_mask_); + for (int i = 0; i < this->rows_; i++) + this->ddr_mask_ &= ~(1 << i); + for (int i = 8; i < (this->cols_ * 2); i++) + this->ddr_mask_ |= (1 << i); + this->write_byte_16(REG_DIR_B, this->ddr_mask_); + + this->read_byte(REG_OPEN_DRAIN_A, &temp_byte); + for (int i = 0; i < this->rows_; i++) + temp_byte |= (1 << i); + this->write_byte(REG_OPEN_DRAIN_A, temp_byte); + + this->read_byte(REG_PULL_UP_B, &temp_byte); + for (int i = 0; i < this->cols_; i++) + temp_byte |= (1 << i); + this->write_byte(REG_PULL_UP_B, temp_byte); + + if (debounce_time_ >= scan_time_) { + debounce_time_ = scan_time_ >> 1; // Force debounce_time to be less than scan_time + } + set_debounce_keypad_(debounce_time_, rows_, cols_); + uint8_t scan_time_bits = 0; + for (uint8_t i = 7; i > 0; i--) { + if (scan_time_ & (1 << i)) { + scan_time_bits = i; + break; + } + } + scan_time_bits &= 0b111; // Scan time is bits 2:0 + temp_byte = sleep_time_ | scan_time_bits; + this->write_byte(REG_KEY_CONFIG_1, temp_byte); + rows_ = (rows_ - 1) & 0b111; // 0 = off, 0b001 = 2 rows, 0b111 = 8 rows, etc. + cols_ = (cols_ - 1) & 0b111; // 0b000 = 1 column, ob111 = 8 columns, etc. + this->write_byte(REG_KEY_CONFIG_2, (rows_ << 3) | cols_); +} + +uint16_t SX1509Component::read_key_data() { + uint16_t key_data; + this->read_byte_16(REG_KEY_DATA_1, &key_data); + return (0xFFFF ^ key_data); +} + +void SX1509Component::set_debounce_config_(uint8_t config_value) { + // First make sure clock is configured + uint8_t temp_byte; + this->read_byte(REG_MISC, &temp_byte); + temp_byte |= (1 << 4); // Just default to no divider if not set + this->write_byte(REG_MISC, temp_byte); + this->read_byte(REG_CLOCK, &temp_byte); + temp_byte |= (1 << 6); // default to internal osc. + this->write_byte(REG_CLOCK, temp_byte); + + config_value &= 0b111; // 3-bit value + this->write_byte(REG_DEBOUNCE_CONFIG, config_value); +} + +void SX1509Component::set_debounce_time_(uint8_t time) { + uint8_t config_value = 0; + + for (int i = 7; i >= 0; i--) { + if (time & (1 << i)) { + config_value = i + 1; + break; + } + } + config_value = constrain(config_value, 0, 7); + + set_debounce_config_(config_value); +} + +void SX1509Component::set_debounce_enable_(uint8_t pin) { + uint16_t debounce_enable; + this->read_byte_16(REG_DEBOUNCE_ENABLE_B, &debounce_enable); + debounce_enable |= (1 << pin); + this->write_byte_16(REG_DEBOUNCE_ENABLE_B, debounce_enable); +} + +void SX1509Component::set_debounce_pin_(uint8_t pin) { set_debounce_enable_(pin); } + +void SX1509Component::set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8_t num_cols) { + set_debounce_time_(time); + for (uint16_t i = 0; i < num_rows; i++) + set_debounce_pin_(i); + for (uint16_t i = 0; i < (8 + num_cols); i++) + set_debounce_pin_(i); +} + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h new file mode 100644 index 0000000000..55d5e54091 --- /dev/null +++ b/esphome/components/sx1509/sx1509.h @@ -0,0 +1,89 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "sx1509_gpio_pin.h" +#include "sx1509_registers.h" + +namespace esphome { +namespace sx1509 { + +// These are used for clock config: +const uint8_t INTERNAL_CLOCK_2MHZ = 2; +const uint8_t EXTERNAL_CLOCK = 1; +const uint8_t SOFTWARE_RESET = 0; +const uint8_t HARDWARE_RESET = 1; + +const uint8_t ANALOG_OUTPUT = 0x03; // To set a pin mode for PWM output + +// PinModes for SX1509 pins +enum SX1509GPIOMode : uint8_t { + SX1509_INPUT = INPUT, // 0x00 + SX1509_INPUT_PULLUP = INPUT_PULLUP, // 0x02 + SX1509_ANALOG_OUTPUT = ANALOG_OUTPUT, // 0x03 + SX1509_OUTPUT = OUTPUT, // 0x01 +}; + +const uint8_t REG_I_ON[16] = {REG_I_ON_0, REG_I_ON_1, REG_I_ON_2, REG_I_ON_3, REG_I_ON_4, REG_I_ON_5, + REG_I_ON_6, REG_I_ON_7, REG_I_ON_8, REG_I_ON_9, REG_I_ON_10, REG_I_ON_11, + REG_I_ON_12, REG_I_ON_13, REG_I_ON_14, REG_I_ON_15}; + +// for all components that implement the process(uint16_t data ) +class SX1509Processor { + public: + virtual void process(uint16_t data){}; +}; + +class SX1509Component : public Component, public i2c::I2CDevice { + public: + SX1509Component() = default; + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + bool digital_read(uint8_t pin); + uint16_t read_key_data(); + void set_pin_value(uint8_t pin, uint8_t i_on) { this->write_byte(REG_I_ON[pin], i_on); }; + void pin_mode(uint8_t pin, uint8_t mode); + void digital_write(uint8_t pin, bool bit_value); + u_long get_clock() { return this->clk_x_; }; + void set_rows_cols(uint8_t rows, uint8_t cols) { + this->rows_ = rows; + this->cols_ = cols; + this->has_keypad_ = true; + }; + void set_sleep_time(uint16_t sleep_time) { this->sleep_time_ = sleep_time; }; + void set_scan_time(uint8_t scan_time) { this->scan_time_ = scan_time; }; + void set_debounce_time(uint8_t debounce_time = 1) { this->debounce_time_ = debounce_time; }; + void register_keypad_binary_sensor(SX1509Processor *binary_sensor) { + this->keypad_binary_sensors_.push_back(binary_sensor); + }; + + protected: + u_long clk_x_ = 2000000; + uint8_t frequency_ = 0; + uint16_t ddr_mask_ = 0x00; + uint16_t input_mask_ = 0x00; + uint16_t port_mask_ = 0x00; + bool has_keypad_ = false; + uint8_t rows_ = 0; + uint8_t cols_ = 0; + uint16_t sleep_time_ = 128; + uint8_t scan_time_ = 1; + uint8_t debounce_time_ = 1; + std::vector<SX1509Processor *> keypad_binary_sensors_; + + void setup_keypad_(); + void set_debounce_config_(uint8_t config_value); + void set_debounce_time_(uint8_t time); + void set_debounce_pin_(uint8_t pin); + void set_debounce_enable_(uint8_t pin); + void set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8_t num_cols); + void setup_led_driver_(uint8_t pin); + void clock_(uint8_t osc_source = 2, uint8_t osc_pin_function = 1, uint8_t osc_freq_out = 0, uint8_t osc_divider = 0); +}; + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/sx1509_gpio_pin.cpp b/esphome/components/sx1509/sx1509_gpio_pin.cpp new file mode 100644 index 0000000000..1d1c87b4e6 --- /dev/null +++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp @@ -0,0 +1,20 @@ +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "sx1509_gpio_pin.h" + +namespace esphome { +namespace sx1509 { + +static const char *TAG = "sx1509_gpio_pin"; + +void SX1509GPIOPin::setup() { + ESP_LOGD(TAG, "setup pin %d", this->pin_); + this->parent_->pin_mode(this->pin_, this->mode_); +} + +void SX1509GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } +bool SX1509GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void SX1509GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/sx1509_gpio_pin.h b/esphome/components/sx1509/sx1509_gpio_pin.h new file mode 100644 index 0000000000..39f841a2a4 --- /dev/null +++ b/esphome/components/sx1509/sx1509_gpio_pin.h @@ -0,0 +1,24 @@ +#pragma once + +#include "sx1509.h" + +namespace esphome { +namespace sx1509 { + +class SX1509Component; + +class SX1509GPIOPin : public GPIOPin { + public: + SX1509GPIOPin(SX1509Component *parent, uint8_t pin, uint8_t mode, bool inverted = false) + : GPIOPin(pin, mode, inverted), parent_(parent){}; + void setup() override; + void pin_mode(uint8_t mode) override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + SX1509Component *parent_; +}; + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/sx1509_registers.h b/esphome/components/sx1509/sx1509_registers.h new file mode 100644 index 0000000000..d73f397f16 --- /dev/null +++ b/esphome/components/sx1509/sx1509_registers.h @@ -0,0 +1,109 @@ +/****************************************************************************** +sx1509_registers.h +Register definitions for SX1509. +Jim Lindblom @ SparkFun Electronics +Original Creation Date: September 21, 2015 +https://github.com/sparkfun/SparkFun_SX1509_Arduino_Library + +Here you'll find the Arduino code used to interface with the SX1509 I2C +16 I/O expander. There are functions to take advantage of everything the +SX1509 provides - input/output setting, writing pins high/low, reading +the input value of pins, LED driver utilities (blink, breath, pwm), and +keypad engine utilites. + +Development environment specifics: + IDE: Arduino 1.6.5 + Hardware Platform: Arduino Uno + SX1509 Breakout Version: v2.0 + +This code is beerware; if you see me (or any other SparkFun employee) at the +local, and you've found our code helpful, please buy us a round! + +Distributed as-is; no warranty is given. +******************************************************************************/ +#pragma once + +namespace esphome { +namespace sx1509 { + +const uint8_t REG_INPUT_DISABLE_B = + 0x00; // RegInputDisableB Input buffer disable register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_INPUT_DISABLE_A = + 0x01; // RegInputDisableA Input buffer disable register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_LONG_SLEW_B = + 0x02; // RegLongSlewB Output buffer long slew register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_LONG_SLEW_A = 0x03; // RegLongSlewA Output buffer long slew register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_LOW_DRIVE_B = + 0x04; // RegLowDriveB Output buffer low drive register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_LOW_DRIVE_A = 0x05; // RegLowDriveA Output buffer low drive register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_PULL_UP_B = 0x06; // RegPullUpB Pull_up register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_PULL_UP_A = 0x07; // RegPullUpA Pull_up register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_PULL_DOWN_B = 0x08; // RegPullDownB Pull_down register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_PULL_DOWN_A = 0x09; // RegPullDownA Pull_down register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_OPEN_DRAIN_B = 0x0A; // RegOpenDrainB Open drain register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_OPEN_DRAIN_A = 0x0B; // RegOpenDrainA Open drain register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_POLARITY_B = 0x0C; // RegPolarityB Polarity register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_POLARITY_A = 0x0D; // RegPolarityA Polarity register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_DIR_B = 0x0E; // RegDirB Direction register _ I/O[15_8] (Bank B) 1111 1111 +const uint8_t REG_DIR_A = 0x0F; // RegDirA Direction register _ I/O[7_0] (Bank A) 1111 1111 +const uint8_t REG_DATA_B = 0x10; // RegDataB Data register _ I/O[15_8] (Bank B) 1111 1111* +const uint8_t REG_DATA_A = 0x11; // RegDataA Data register _ I/O[7_0] (Bank A) 1111 1111* +const uint8_t REG_INTERRUPT_MASK_B = + 0x12; // RegInterruptMaskB Interrupt mask register _ I/O[15_8] (Bank B) 1111 1111 +const uint8_t REG_INTERRUPT_MASK_A = + 0x13; // RegInterruptMaskA Interrupt mask register _ I/O[7_0] (Bank A) 1111 1111 +const uint8_t REG_SENSE_HIGH_B = 0x14; // RegSenseHighB Sense register for I/O[15:12] 0000 0000 +const uint8_t REG_SENSE_LOW_B = 0x15; // RegSenseLowB Sense register for I/O[11:8] 0000 0000 +const uint8_t REG_SENSE_HIGH_A = 0x16; // RegSenseHighA Sense register for I/O[7:4] 0000 0000 +const uint8_t REG_SENSE_LOW_A = 0x17; // RegSenseLowA Sense register for I/O[3:0] 0000 0000 +const uint8_t REG_INTERRUPT_SOURCE_B = + 0x18; // RegInterruptSourceB Interrupt source register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_INTERRUPT_SOURCE_A = + 0x19; // RegInterruptSourceA Interrupt source register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_EVENT_STATUS_B = 0x1A; // RegEventStatusB Event status register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_EVENT_STATUS_A = 0x1B; // RegEventStatusA Event status register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_LEVEL_SHIFTER_1 = 0x1C; // RegLevelShifter1 Level shifter register 0000 0000 +const uint8_t REG_LEVEL_SHIFTER_2 = 0x1D; // RegLevelShifter2 Level shifter register 0000 0000 +const uint8_t REG_CLOCK = 0x1E; // RegClock Clock management register 0000 0000 +const uint8_t REG_MISC = 0x1F; // RegMisc Miscellaneous device settings register 0000 0000 +const uint8_t REG_LED_DRIVER_ENABLE_B = + 0x20; // RegLEDDriverEnableB LED driver enable register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_LED_DRIVER_ENABLE_A = + 0x21; // RegLEDDriverEnableA LED driver enable register _ I/O[7_0] (Bank A) 0000 0000 +// Debounce and Keypad Engine +const uint8_t REG_DEBOUNCE_CONFIG = 0x22; // RegDebounceConfig Debounce configuration register 0000 0000 +const uint8_t REG_DEBOUNCE_ENABLE_B = + 0x23; // RegDebounceEnableB Debounce enable register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_DEBOUNCE_ENABLE_A = + 0x24; // RegDebounceEnableA Debounce enable register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_KEY_CONFIG_1 = 0x25; // RegKeyConfig1 Key scan configuration register 0000 0000 +const uint8_t REG_KEY_CONFIG_2 = 0x26; // RegKeyConfig2 Key scan configuration register 0000 0000 +const uint8_t REG_KEY_DATA_1 = 0x27; // RegKeyData1 Key value (column) 1111 1111 +const uint8_t REG_KEY_DATA_2 = 0x28; // RegKeyData2 Key value (row) 1111 1111 +// LED Driver (PWM, blinking, breathing) +const uint8_t REG_I_ON_0 = 0x2A; // RegIOn0 ON intensity register for I/O[0] 1111 1111 +const uint8_t REG_I_ON_1 = 0x2D; // RegIOn1 ON intensity register for I/O[1] 1111 1111 +const uint8_t REG_I_ON_2 = 0x30; // RegIOn2 ON intensity register for I/O[2] 1111 1111 +const uint8_t REG_I_ON_3 = 0x33; // RegIOn3 ON intensity register for I/O[3] 1111 1111 +const uint8_t REG_I_ON_4 = 0x36; // RegIOn4 ON intensity register for I/O[4] 1111 1111 +const uint8_t REG_I_ON_5 = 0x3B; // RegIOn5 ON intensity register for I/O[5] 1111 1111 +const uint8_t REG_I_ON_6 = 0x40; // RegIOn6 ON intensity register for I/O[6] 1111 1111 +const uint8_t REG_I_ON_7 = 0x45; // RegIOn7 ON intensity register for I/O[7] 1111 1111 +const uint8_t REG_I_ON_8 = 0x4A; // RegIOn8 ON intensity register for I/O[8] 1111 1111 +const uint8_t REG_I_ON_9 = 0x4D; // RegIOn9 ON intensity register for I/O[9] 1111 1111 +const uint8_t REG_I_ON_10 = 0x50; // RegIOn10 ON intensity register for I/O[10] 1111 1111 +const uint8_t REG_I_ON_11 = 0x53; // RegIOn11 ON intensity register for I/O[11] 1111 1111 +const uint8_t REG_I_ON_12 = 0x56; // RegIOn12 ON intensity register for I/O[12] 1111 1111 +const uint8_t REG_I_ON_13 = 0x5B; // RegIOn13 ON intensity register for I/O[13] 1111 1111 +const uint8_t REG_I_ON_14 = 0x60; // RegIOn14 ON intensity register for I/O[14] 1111 1111 +const uint8_t REG_I_ON_15 = 0x65; // RegIOn15 ON intensity register for I/O[15] 1111 1111 +// Miscellaneous +const uint8_t REG_HIGH_INPUT_B = 0x69; // RegHighInputB High input enable register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_HIGH_INPUT_A = 0x6A; // RegHighInputA High input enable register _ I/O[7_0] (Bank A) 0000 0000 +// Software Reset +const uint8_t REG_RESET = 0x7D; // RegReset Software reset register 0000 0000 +const uint8_t REG_TEST_1 = 0x7E; // RegTest1 Test register 0000 0000 +const uint8_t REG_TEST_2 = 0x7F; // RegTest2 Test register 0000 0000 + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 50fef7b125..3c94f4a243 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -1,36 +1,18 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, sensor -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.components import climate_ir +from esphome.const import CONF_ID -AUTO_LOAD = ['sensor'] +AUTO_LOAD = ['climate_ir'] tcl112_ns = cg.esphome_ns.namespace('tcl112') -Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate.Climate, cg.Component) +Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate_ir.ClimateIR) -CONF_TRANSMITTER_ID = 'transmitter_id' -CONF_SUPPORTS_HEAT = 'supports_heat' -CONF_SUPPORTS_COOL = 'supports_cool' - -CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(Tcl112Climate), - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), - cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, - cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, - cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), -}).extend(cv.COMPONENT_SCHEMA)) +}) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield climate.register_climate(var, config) - - cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) - cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) - if CONF_SENSOR in config: - sens = yield cg.get_variable(config[CONF_SENSOR]) - cg.add(var.set_sensor(sens)) - - transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) - cg.add(var.set_transmitter(transmitter)) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index cbe2f53402..2907ae1743 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -18,62 +18,15 @@ const uint8_t TCL112_AUTO = 8; const uint8_t TCL112_POWER_MASK = 0x04; const uint8_t TCL112_HALF_DEGREE = 0b00100000; -const float TCL112_TEMP_MAX = 31.0; -const float TCL112_TEMP_MIN = 16.0; -const uint16_t TCL112_HEADER_MARK = 3000; +const uint16_t TCL112_HEADER_MARK = 3100; const uint16_t TCL112_HEADER_SPACE = 1650; const uint16_t TCL112_BIT_MARK = 500; -const uint16_t TCL112_ONE_SPACE = 1050; -const uint16_t TCL112_ZERO_SPACE = 325; +const uint16_t TCL112_ONE_SPACE = 1100; +const uint16_t TCL112_ZERO_SPACE = 350; const uint32_t TCL112_GAP = TCL112_HEADER_SPACE; -climate::ClimateTraits Tcl112Climate::traits() { - auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_auto_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_two_point_target_temperature(false); - traits.set_supports_away(false); - traits.set_visual_min_temperature(TCL112_TEMP_MIN); - traits.set_visual_max_temperature(TCL112_TEMP_MAX); - traits.set_visual_temperature_step(.5f); - return traits; -} - -void Tcl112Climate::setup() { - if (this->sensor_) { - this->sensor_->add_on_state_callback([this](float state) { - this->current_temperature = state; - // current temperature changed, publish state - this->publish_state(); - }); - this->current_temperature = this->sensor_->state; - } else - this->current_temperature = NAN; - // restore set points - auto restore = this->restore_state_(); - if (restore.has_value()) { - restore->apply(this); - } else { - // restore from defaults - this->mode = climate::CLIMATE_MODE_OFF; - this->target_temperature = 24; - } -} - -void Tcl112Climate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - - this->transmit_state_(); - this->publish_state(); -} - -void Tcl112Climate::transmit_state_() { +void Tcl112Climate::transmit_state() { uint8_t remote_state[TCL112_STATE_LENGTH] = {0}; // A known good state. (On, Cool, 24C) @@ -124,7 +77,10 @@ void Tcl112Climate::transmit_state_() { for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) remote_state[TCL112_STATE_LENGTH - 1] += remote_state[checksum_byte]; - ESP_LOGV(TAG, "Sending tcl code: %u", remote_state[7]); + ESP_LOGV(TAG, "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0], + remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], + remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12], + remote_state[13]); auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); @@ -148,5 +104,76 @@ void Tcl112Climate::transmit_state_() { transmit.perform(); } +bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(TCL112_HEADER_MARK, TCL112_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + uint8_t remote_state[TCL112_STATE_LENGTH] = {0}; + // Read all bytes. + for (int i = 0; i < TCL112_STATE_LENGTH; i++) { + // Read bit + for (int j = 0; j < 8; j++) { + if (data.expect_item(TCL112_BIT_MARK, TCL112_ONE_SPACE)) + remote_state[i] |= 1 << j; + else if (!data.expect_item(TCL112_BIT_MARK, TCL112_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + } + // Validate footer + if (!data.expect_mark(TCL112_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + uint8_t checksum = 0; + // Calculate & set the checksum for the current internal state of the remote. + // Stored the checksum value in the last byte. + for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) + checksum += remote_state[checksum_byte]; + if (checksum != remote_state[TCL112_STATE_LENGTH - 1]) { + ESP_LOGV(TAG, "Checksum fail"); + return false; + } + + ESP_LOGV(TAG, "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", + remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], + remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], + remote_state[12], remote_state[13]); + + // two first bytes are constant + if (remote_state[0] != 0x23 || remote_state[1] != 0xCB) + return false; + + if ((remote_state[5] & TCL112_POWER_MASK) == 0) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + auto mode = remote_state[6] & 0x0F; + switch (mode) { + case TCL112_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case TCL112_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case TCL112_DRY: + case TCL112_FAN: + case TCL112_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + } + } + auto temp = TCL112_TEMP_MAX - remote_state[7]; + if (remote_state[12] & TCL112_HALF_DEGREE) + temp += .5f; + this->target_temperature = temp; + this->publish_state(); + return true; +} + } // namespace tcl112 } // namespace esphome diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h index 0b80dedbef..273162662d 100644 --- a/esphome/components/tcl112/tcl112.h +++ b/esphome/components/tcl112/tcl112.h @@ -1,39 +1,23 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/remote_base/remote_base.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" -#include "esphome/components/sensor/sensor.h" +#include "esphome/components/climate_ir/climate_ir.h" namespace esphome { namespace tcl112 { -class Tcl112Climate : public climate::Climate, public Component { +// Temperature +const float TCL112_TEMP_MAX = 31.0; +const float TCL112_TEMP_MIN = 16.0; + +class Tcl112Climate : public climate_ir::ClimateIR { public: - void setup() override; - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } - void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } - void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } - void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + Tcl112Climate() : climate_ir::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f) {} 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; - /// Transmit via IR the state of this climate controller. - void transmit_state_(); - - bool supports_cool_{true}; - bool supports_heat_{true}; - - remote_transmitter::RemoteTransmitterComponent *transmitter_; - sensor::Sensor *sensor_{nullptr}; + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; }; } // namespace tcl112 diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 808318ac81..13370d749c 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -4,7 +4,7 @@ from esphome import automation from esphome.components import cover from esphome.const import CONF_ASSUMED_STATE, CONF_CLOSE_ACTION, CONF_CURRENT_OPERATION, CONF_ID, \ CONF_LAMBDA, CONF_OPEN_ACTION, CONF_OPTIMISTIC, CONF_POSITION, CONF_RESTORE_MODE, \ - CONF_STATE, CONF_STOP_ACTION + CONF_STATE, CONF_STOP_ACTION, CONF_TILT, CONF_TILT_ACTION, CONF_TILT_LAMBDA from .. import template_ns TemplateCover = template_ns.class_('TemplateCover', cover.Cover, cg.Component) @@ -24,6 +24,8 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, cv.Optional(CONF_RESTORE_MODE, default='RESTORE'): cv.enum(RESTORE_MODES, upper=True), }).extend(cv.COMPONENT_SCHEMA) @@ -42,6 +44,14 @@ def to_code(config): yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]) if CONF_STOP_ACTION in config: yield automation.build_automation(var.get_stop_trigger(), [], config[CONF_STOP_ACTION]) + if CONF_TILT_ACTION in config: + yield automation.build_automation(var.get_tilt_trigger(), [(float, 'tilt')], + config[CONF_TILT_ACTION]) + cg.add(var.set_has_tilt(True)) + if CONF_TILT_LAMBDA in config: + tilt_template_ = yield cg.process_lambda(config[CONF_TILT_LAMBDA], [], + return_type=cg.optional.template(float)) + cg.add(var.set_tilt_lambda(tilt_template_)) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) @@ -53,6 +63,7 @@ def to_code(config): cv.Exclusive(CONF_STATE, 'pos'): cv.templatable(cover.validate_cover_state), cv.Exclusive(CONF_POSITION, 'pos'): cv.templatable(cv.zero_to_one_float), cv.Optional(CONF_CURRENT_OPERATION): cv.templatable(cover.validate_cover_operation), + cv.Optional(CONF_TILT): cv.templatable(cv.zero_to_one_float), })) def cover_template_publish_to_code(config, action_id, template_arg, args): paren = yield cg.get_variable(config[CONF_ID]) @@ -63,6 +74,9 @@ def cover_template_publish_to_code(config, action_id, template_arg, args): if CONF_POSITION in config: template_ = yield cg.templatable(config[CONF_POSITION], args, float) cg.add(var.set_position(template_)) + if CONF_TILT in config: + template_ = yield cg.templatable(config[CONF_TILT], args, float) + cg.add(var.set_tilt(template_)) if CONF_CURRENT_OPERATION in config: template_ = yield cg.templatable(config[CONF_CURRENT_OPERATION], args, cover.CoverOperation) cg.add(var.set_current_operation(template_)) diff --git a/esphome/components/template/output/__init__.py b/esphome/components/template/output/__init__.py index 5cc9e089bd..cc85a9da68 100644 --- a/esphome/components/template/output/__init__.py +++ b/esphome/components/template/output/__init__.py @@ -2,13 +2,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import output -from esphome.const import CONF_ID, CONF_TYPE +from esphome.const import CONF_ID, CONF_TYPE, CONF_BINARY from .. import template_ns TemplateBinaryOutput = template_ns.class_('TemplateBinaryOutput', output.BinaryOutput) TemplateFloatOutput = template_ns.class_('TemplateFloatOutput', output.FloatOutput) -CONF_BINARY = 'binary' CONF_FLOAT = 'float' CONF_WRITE_ACTION = 'write_action' diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 719f0b0d62..85c2b644a0 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -8,12 +8,12 @@ namespace text_sensor { #define LOG_TEXT_SENSOR(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Icon: '%s'", obj->get_icon().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ } \ if (!obj->unique_id().empty()) { \ - ESP_LOGV(TAG, prefix " Unique ID: '%s'", obj->unique_id().c_str()); \ + ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, obj->unique_id().c_str()); \ } \ } diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 634de26f00..ca1ac375ba 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -2,6 +2,7 @@ import bisect import datetime import logging import math +import string import pytz import tzlocal @@ -52,8 +53,18 @@ def _tz_dst_str(dt): _tz_timedelta(td)) -def _non_dst_tz(tz, dt): +def _safe_tzname(tz, dt): tzname = tz.tzname(dt) + # pytz does not always return valid tznames + # For example: 'Europe/Saratov' returns '+04' + # Work around it by using a generic name for the timezone + if not all(c in string.ascii_letters for c in tzname): + return 'TZ' + return tzname + + +def _non_dst_tz(tz, dt): + tzname = _safe_tzname(tz, dt) utcoffset = tz.utcoffset(dt) _LOGGER.info("Detected timezone '%s' with UTC offset %s", tzname, _tz_timedelta(utcoffset)) @@ -104,8 +115,9 @@ def convert_tz(pytz_obj): _tz_dst_str(dst_begins_local), _tz_dst_str(dst_ends_local)) _LOGGER.info("Detected timezone '%s' with UTC offset %s and daylight savings time from " "%s to %s", - tzname_off, _tz_timedelta(utcoffset_off), dst_begins_local.strftime("%x %X"), - dst_ends_local.strftime("%x %X")) + tzname_off, _tz_timedelta(utcoffset_off), + dst_begins_local.strftime("%d %B %X"), + dst_ends_local.strftime("%d %B %X")) return tzbase + tzext diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 81524826be..cb66dc3ce6 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -12,7 +12,6 @@ static const char *TAG = "time"; RealTimeClock::RealTimeClock() = default; void RealTimeClock::call_setup() { - this->setup_internal_(); setenv("TZ", this->timezone_.c_str(), 1); tzset(); this->setup(); @@ -85,12 +84,12 @@ template<typename T> bool increment_time_value(T ¤t, uint16_t begin, uint1 static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } -static bool days_in_month(uint8_t month, uint16_t year) { +static uint8_t days_in_month(uint8_t month, uint16_t year) { static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - uint8_t days_in_month = DAYS_IN_MONTH[month]; + uint8_t days = DAYS_IN_MONTH[month]; if (month == 2 && is_leap_year(year)) - days_in_month = 29; - return days_in_month; + return 29; + return days; } void ESPTime::increment_second() { @@ -128,13 +127,13 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { return; } - for (uint16_t i = 1970; i < this->year; i++) + for (int i = 1970; i < this->year; i++) res += is_leap_year(i) ? 366 : 365; if (use_day_of_year) { res += this->day_of_year - 1; } else { - for (uint8_t i = 1; i < this->month; ++i) + for (int i = 1; i < this->month; i++) res += days_in_month(i, this->year); res += this->day_of_month - 1; diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover.py index 85f606e6cc..6a7c9b6835 100644 --- a/esphome/components/time_based/cover.py +++ b/esphome/components/time_based/cover.py @@ -8,6 +8,8 @@ from esphome.const import CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, CONF_ID, CONF_ time_based_ns = cg.esphome_ns.namespace('time_based') TimeBasedCover = time_based_ns.class_('TimeBasedCover', cover.Cover, cg.Component) +CONF_HAS_BUILT_IN_ENDSTOP = 'has_built_in_endstop' + CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(TimeBasedCover), cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), @@ -17,6 +19,8 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + + cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, }).extend(cv.COMPONENT_SCHEMA) @@ -32,3 +36,5 @@ def to_code(config): cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]) + + cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP])) diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index bbc887debc..bdb4e5379c 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -30,13 +30,19 @@ void TimeBasedCover::loop() { // Recompute position every loop cycle this->recompute_position_(); - if (this->current_operation != COVER_OPERATION_IDLE && this->is_at_target_()) { - this->start_direction_(COVER_OPERATION_IDLE); + if (this->is_at_target_()) { + if (this->has_built_in_endstop_ && + (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) { + // Don't trigger stop, let the cover stop by itself. + this->current_operation = COVER_OPERATION_IDLE; + } else { + this->start_direction_(COVER_OPERATION_IDLE); + } this->publish_state(); } // Send current position every second - if (this->current_operation != COVER_OPERATION_IDLE && now - this->last_publish_time_ > 1000) { + if (now - this->last_publish_time_ > 1000) { this->publish_state(false); this->last_publish_time_ = now; } @@ -57,6 +63,12 @@ void TimeBasedCover::control(const CoverCall &call) { auto pos = *call.get_position(); if (pos == this->position) { // already at target + // for covers with built in end stop, we should send the command again + if (this->has_built_in_endstop_ && (pos == COVER_OPEN || pos == COVER_CLOSED)) { + auto op = pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING; + this->target_position_ = pos; + this->start_direction_(op); + } } else { auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING; this->target_position_ = pos; @@ -82,7 +94,7 @@ bool TimeBasedCover::is_at_target_() const { } } void TimeBasedCover::start_direction_(CoverOperation dir) { - if (dir == this->current_operation) + if (dir == this->current_operation && dir != COVER_OPERATION_IDLE) return; this->recompute_position_(); diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index 60819d797b..be3a55c546 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -20,6 +20,7 @@ class TimeBasedCover : public cover::Cover, public Component { void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; } cover::CoverTraits get_traits() override; + void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; } protected: void control(const cover::CoverCall &call) override; @@ -41,6 +42,7 @@ class TimeBasedCover : public cover::Cover, public Component { uint32_t start_dir_time_{0}; uint32_t last_publish_time_{0}; float target_position_{0}; + bool has_built_in_endstop_{false}; }; } // namespace time_based diff --git a/esphome/components/tlc59208f/__init__.py b/esphome/components/tlc59208f/__init__.py new file mode 100644 index 0000000000..4666b63b46 --- /dev/null +++ b/esphome/components/tlc59208f/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +DEPENDENCIES = ['i2c'] +MULTI_CONF = True + +tlc59208f_ns = cg.esphome_ns.namespace('tlc59208f') +TLC59208FOutput = tlc59208f_ns.class_('TLC59208FOutput', cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(TLC59208FOutput), +}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/tlc59208f/output.py b/esphome/components/tlc59208f/output.py new file mode 100644 index 0000000000..f61f7729e7 --- /dev/null +++ b/esphome/components/tlc59208f/output.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import TLC59208FOutput, tlc59208f_ns + +DEPENDENCIES = ['tlc59208f'] + +TLC59208FChannel = tlc59208f_ns.class_('TLC59208FChannel', output.FloatOutput) +CONF_TLC59208F_ID = 'tlc59208f_id' + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ + cv.Required(CONF_ID): cv.declare_id(TLC59208FChannel), + cv.GenerateID(CONF_TLC59208F_ID): cv.use_id(TLC59208FOutput), + + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), +}) + + +def to_code(config): + paren = yield cg.get_variable(config[CONF_TLC59208F_ID]) + rhs = paren.create_channel(config[CONF_CHANNEL]) + var = cg.Pvariable(config[CONF_ID], rhs) + yield output.register_output(var, config) diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp new file mode 100644 index 0000000000..6e65ff4e76 --- /dev/null +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -0,0 +1,155 @@ +#include "tlc59208f_output.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tlc59208f { + +static const char *TAG = "tlc59208f"; + +// * marks register defaults +// 0*: Register auto increment disabled, 1: Register auto increment enabled +const uint8_t TLC59208F_MODE1_AI2 = (1 << 7); +// 0*: don't auto increment bit 1, 1: auto increment bit 1 +const uint8_t TLC59208F_MODE1_AI1 = (1 << 6); +// 0*: don't auto increment bit 0, 1: auto increment bit 0 +const uint8_t TLC59208F_MODE1_AI0 = (1 << 5); +// 0: normal mode, 1*: low power mode, osc off +const uint8_t TLC59208F_MODE1_SLEEP = (1 << 4); +// 0*: device doesn't respond to i2c bus sub-address 1, 1: responds +const uint8_t TLC59208F_MODE1_SUB1 = (1 << 3); +// 0*: device doesn't respond to i2c bus sub-address 2, 1: responds +const uint8_t TLC59208F_MODE1_SUB2 = (1 << 2); +// 0*: device doesn't respond to i2c bus sub-address 3, 1: responds +const uint8_t TLC59208F_MODE1_SUB3 = (1 << 1); +// 0: device doesn't respond to i2c all-call 3, 1*: responds to all-call +const uint8_t TLC59208F_MODE1_ALLCALL = (1 << 0); + +// 0*: Group dimming, 1: Group blinking +const uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); +// 0*: Output change on Stop command, 1: Output change on ACK +const uint8_t TLC59208F_MODE2_OCH = (1 << 3); +// 0*: WDT disabled, 1: WDT enabled +const uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); +// WDT timeouts +const uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); +const uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); +const uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); +const uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); + +// --- Special function --- +// Call address to perform software reset, no devices will ACK +const uint8_t TLC59208F_SWRST_ADDR = 0x96; //(0x4b 7-bit addr + ~W) +const uint8_t TLC59208F_SWRST_SEQ[2] = {0xa5, 0x5a}; + +// --- Registers ---2 +// Mode register 1 +const uint8_t TLC59208F_REG_MODE1 = 0x00; +// Mode register 2 +const uint8_t TLC59208F_REG_MODE2 = 0x01; +// PWM0 +const uint8_t TLC59208F_REG_PWM0 = 0x02; +// Group PWM +const uint8_t TLC59208F_REG_GROUPPWM = 0x0a; +// Group Freq +const uint8_t TLC59208F_REG_GROUPFREQ = 0x0b; +// LEDOUTx registers +const uint8_t TLC59208F_REG_LEDOUT0 = 0x0c; +const uint8_t TLC59208F_REG_LEDOUT1 = 0x0d; +// Sub-address registers +const uint8_t TLC59208F_REG_SUBADR1 = 0x0e; // default: 0x92 (8-bit addr) +const uint8_t TLC59208F_REG_SUBADR2 = 0x0f; // default: 0x94 (8-bit addr) +const uint8_t TLC59208F_REG_SUBADR3 = 0x10; // default: 0x98 (8-bit addr) +// All call address register +const uint8_t TLC59208F_REG_ALLCALLADR = 0x11; // default: 0xd0 (8-bit addr) + +// --- Output modes --- +static const uint8_t LDR_OFF = 0x00; +static const uint8_t LDR_ON = 0x01; +static const uint8_t LDR_PWM = 0x02; +static const uint8_t LDR_GRPPWM = 0x03; + +void TLC59208FOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up TLC59208FOutputComponent..."); + + ESP_LOGV(TAG, " Resetting all devices on the bus..."); + + // Reset all devices on the bus + if (!this->parent_->write_byte(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ[0], TLC59208F_SWRST_SEQ[1])) { + ESP_LOGE(TAG, "RESET failed"); + this->mark_failed(); + return; + } + + // Auto increment registers, and respond to all-call address + if (!this->write_byte(TLC59208F_REG_MODE1, TLC59208F_MODE1_AI2 | TLC59208F_MODE1_ALLCALL)) { + ESP_LOGE(TAG, "MODE1 failed"); + this->mark_failed(); + return; + } + if (!this->write_byte(TLC59208F_REG_MODE2, this->mode_)) { + ESP_LOGE(TAG, "MODE2 failed"); + this->mark_failed(); + return; + } + // Set all 3 outputs to be individually controlled + // TODO: think of a way to support group dimming + if (!this->write_byte(TLC59208F_REG_LEDOUT0, (LDR_PWM << 6) | (LDR_PWM << 4) | (LDR_PWM << 2) | (LDR_PWM << 0))) { + ESP_LOGE(TAG, "LEDOUT0 failed"); + this->mark_failed(); + return; + } + if (!this->write_byte(TLC59208F_REG_LEDOUT1, (LDR_PWM << 6) | (LDR_PWM << 4) | (LDR_PWM << 2) | (LDR_PWM << 0))) { + ESP_LOGE(TAG, "LEDOUT1 failed"); + this->mark_failed(); + return; + } + delayMicroseconds(500); + + this->loop(); +} + +void TLC59208FOutput::dump_config() { + ESP_LOGCONFIG(TAG, "TLC59208F:"); + ESP_LOGCONFIG(TAG, " Mode: 0x%02X", this->mode_); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Setting up TLC59208F failed!"); + } +} + +void TLC59208FOutput::loop() { + if (this->min_channel_ == 0xFF || !this->update_) + return; + + for (uint8_t channel = this->min_channel_; channel <= this->max_channel_; channel++) { + uint8_t pwm = this->pwm_amounts_[channel]; + ESP_LOGVV(TAG, "Channel %02u: pwm=%04u ", channel, pwm); + + uint8_t reg = TLC59208F_REG_PWM0 + channel; + if (!this->write_byte(reg, pwm)) { + this->status_set_warning(); + return; + } + } + + this->status_clear_warning(); + this->update_ = false; +} + +TLC59208FChannel *TLC59208FOutput::create_channel(uint8_t channel) { + this->min_channel_ = std::min(this->min_channel_, channel); + this->max_channel_ = std::max(this->max_channel_, channel); + auto *c = new TLC59208FChannel(this, channel); + return c; +} + +void TLC59208FChannel::write_state(float state) { + const uint8_t max_duty = 255; + const float duty_rounded = roundf(state * max_duty); + auto duty = static_cast<uint8_t>(duty_rounded); + this->parent_->set_channel_value_(this->channel_, duty); +} + +} // namespace tlc59208f +} // namespace esphome diff --git a/esphome/components/tlc59208f/tlc59208f_output.h b/esphome/components/tlc59208f/tlc59208f_output.h new file mode 100644 index 0000000000..06b7adc882 --- /dev/null +++ b/esphome/components/tlc59208f/tlc59208f_output.h @@ -0,0 +1,67 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tlc59208f { + +// 0*: Group dimming, 1: Group blinking +extern const uint8_t TLC59208F_MODE2_DMBLNK; +// 0*: Output change on Stop command, 1: Output change on ACK +extern const uint8_t TLC59208F_MODE2_OCH; +// 0*: WDT disabled, 1: WDT enabled +extern const uint8_t TLC59208F_MODE2_WDTEN; +// WDT timeouts +extern const uint8_t TLC59208F_MODE2_WDT_5MS; +extern const uint8_t TLC59208F_MODE2_WDT_15MS; +extern const uint8_t TLC59208F_MODE2_WDT_25MS; +extern const uint8_t TLC59208F_MODE2_WDT_35MS; + +class TLC59208FOutput; + +class TLC59208FChannel : public output::FloatOutput { + public: + TLC59208FChannel(TLC59208FOutput *parent, uint8_t channel) : parent_(parent), channel_(channel) {} + + protected: + void write_state(float state) override; + + TLC59208FOutput *parent_; + uint8_t channel_; +}; + +/// TLC59208F float output component. +class TLC59208FOutput : public Component, public i2c::I2CDevice { + public: + TLC59208FOutput(uint8_t mode = TLC59208F_MODE2_OCH) : mode_(mode) {} + + TLC59208FChannel *create_channel(uint8_t channel); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + protected: + friend TLC59208FChannel; + + void set_channel_value_(uint8_t channel, uint8_t value) { + if (this->pwm_amounts_[channel] != value) + this->update_ = true; + this->pwm_amounts_[channel] = value; + } + + uint8_t mode_; + + uint8_t min_channel_{0xFF}; + uint8_t max_channel_{0x00}; + uint8_t pwm_amounts_[256] = { + 0, + }; + bool update_{true}; +}; + +} // namespace tlc59208f +} // namespace esphome diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py new file mode 100644 index 0000000000..541f10f862 --- /dev/null +++ b/esphome/components/tuya/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +DEPENDENCIES = ['uart'] + +tuya_ns = cg.esphome_ns.namespace('tuya') +Tuya = tuya_ns.class_('Tuya', cg.Component, uart.UARTDevice) + +CONF_TUYA_ID = 'tuya_id' +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Tuya), +}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py new file mode 100644 index 0000000000..605bdae32e --- /dev/null +++ b/esphome/components/tuya/light/__init__.py @@ -0,0 +1,44 @@ +from esphome.components import light +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE, CONF_GAMMA_CORRECT, \ + CONF_DEFAULT_TRANSITION_LENGTH +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] + +CONF_DIMMER_DATAPOINT = "dimmer_datapoint" +CONF_SWITCH_DATAPOINT = "switch_datapoint" + +TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component) + +CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_DIMMER_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_MIN_VALUE): cv.int_, + cv.Optional(CONF_MAX_VALUE): cv.int_, + + # Change the default gamma_correct and default transition length settings. + # The Tuya MCU handles transitions and gamma correction on its own. + cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, + cv.Optional(CONF_DEFAULT_TRANSITION_LENGTH, default='0s'): cv.positive_time_period_milliseconds, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + yield cg.register_component(var, config) + yield light.register_light(var, config) + + if CONF_DIMMER_DATAPOINT in config: + cg.add(var.set_dimmer_id(config[CONF_DIMMER_DATAPOINT])) + if CONF_SWITCH_DATAPOINT in config: + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_MIN_VALUE in config: + cg.add(var.set_min_value(config[CONF_MIN_VALUE])) + if CONF_MAX_VALUE in config: + cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp new file mode 100644 index 0000000000..9696252049 --- /dev/null +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -0,0 +1,85 @@ +#include "esphome/core/log.h" +#include "tuya_light.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.light"; + +void TuyaLight::setup() { + if (this->dimmer_id_.has_value()) { + this->parent_->register_listener(*this->dimmer_id_, [this](TuyaDatapoint datapoint) { + auto call = this->state_->make_call(); + call.set_brightness(float(datapoint.value_uint) / this->max_value_); + call.perform(); + }); + } + if (switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + auto call = this->state_->make_call(); + call.set_state(datapoint.value_bool); + call.perform(); + }); + } +} + +void TuyaLight::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Dimmer:"); + if (this->dimmer_id_.has_value()) + ESP_LOGCONFIG(TAG, " Dimmer has datapoint ID %u", *this->dimmer_id_); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); +} + +light::LightTraits TuyaLight::get_traits() { + auto traits = light::LightTraits(); + traits.set_supports_brightness(this->dimmer_id_.has_value()); + return traits; +} + +void TuyaLight::setup_state(light::LightState *state) { state_ = state; } + +void TuyaLight::write_state(light::LightState *state) { + float brightness; + state->current_values_as_brightness(&brightness); + + if (brightness == 0.0f) { + // turning off, first try via switch (if exists), then dimmer + if (switch_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = false; + + parent_->set_datapoint_value(datapoint); + } else if (dimmer_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->dimmer_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = 0; + parent_->set_datapoint_value(datapoint); + } + return; + } + + auto brightness_int = static_cast<uint32_t>(brightness * this->max_value_); + brightness_int = std::max(brightness_int, this->min_value_); + + if (this->dimmer_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->dimmer_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = brightness_int; + parent_->set_datapoint_value(datapoint); + } + if (this->switch_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = true; + parent_->set_datapoint_value(datapoint); + } +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h new file mode 100644 index 0000000000..581512c29c --- /dev/null +++ b/esphome/components/tuya/light/tuya_light.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/light/light_output.h" + +namespace esphome { +namespace tuya { + +class TuyaLight : public Component, public light::LightOutput { + public: + void setup() override; + void dump_config() override; + void set_dimmer_id(uint8_t dimmer_id) { this->dimmer_id_ = dimmer_id; } + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + void set_min_value(uint32_t min_value) { min_value_ = min_value; } + void set_max_value(uint32_t max_value) { max_value_ = max_value; } + light::LightTraits get_traits() override; + void setup_state(light::LightState *state) override; + void write_state(light::LightState *state) override; + + protected: + void update_dimmer_(uint32_t value); + void update_switch_(uint32_t value); + + Tuya *parent_; + optional<uint8_t> dimmer_id_{}; + optional<uint8_t> switch_id_{}; + uint32_t min_value_ = 0; + uint32_t max_value_ = 255; + light::LightState *state_{nullptr}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp new file mode 100644 index 0000000000..cb796644c8 --- /dev/null +++ b/esphome/components/tuya/tuya.cpp @@ -0,0 +1,298 @@ +#include "tuya.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya"; + +void Tuya::setup() { + this->send_empty_command_(TuyaCommandType::MCU_CONF); + this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); +} + +void Tuya::loop() { + while (this->available()) { + uint8_t c; + this->read_byte(&c); + this->handle_char_(c); + } +} + +void Tuya::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya:"); + if ((gpio_status_ != -1) || (gpio_reset_ != -1)) + ESP_LOGCONFIG(TAG, " GPIO MCU configuration not supported!"); + for (auto &info : this->datapoints_) { + if (info.type == TuyaDatapointType::BOOLEAN) + ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool)); + else if (info.type == TuyaDatapointType::INTEGER) + ESP_LOGCONFIG(TAG, " Datapoint %d: int value (value: %d)", info.id, info.value_int); + else if (info.type == TuyaDatapointType::ENUM) + ESP_LOGCONFIG(TAG, " Datapoint %d: enum (value: %d)", info.id, info.value_enum); + else if (info.type == TuyaDatapointType::BITMASK) + ESP_LOGCONFIG(TAG, " Datapoint %d: bitmask (value: %x)", info.id, info.value_bitmask); + else + ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); + } + if (this->datapoints_.empty()) { + ESP_LOGCONFIG(TAG, " Received no datapoints! Please make sure this is a supported Tuya device."); + } + this->check_uart_settings(9600); +} + +bool Tuya::validate_message_() { + uint32_t at = this->rx_message_.size() - 1; + auto *data = &this->rx_message_[0]; + uint8_t new_byte = data[at]; + + // Byte 0: HEADER1 (always 0x55) + if (at == 0) + return new_byte == 0x55; + // Byte 1: HEADER2 (always 0xAA) + if (at == 1) + return new_byte == 0xAA; + + // Byte 2: VERSION + // no validation for the following fields: + uint8_t version = data[2]; + if (at == 2) + return true; + // Byte 3: COMMAND + uint8_t command = data[3]; + if (at == 3) + return true; + + // Byte 4: LENGTH1 + // Byte 5: LENGTH2 + if (at <= 5) + // no validation for these fields + return true; + + uint16_t length = (uint16_t(data[4]) << 8) | (uint16_t(data[5])); + + // wait until all data is read + if (at - 6 < length) + return true; + + // Byte 6+LEN: CHECKSUM - sum of all bytes (including header) modulo 256 + uint8_t rx_checksum = new_byte; + uint8_t calc_checksum = 0; + for (uint32_t i = 0; i < 6 + length; i++) + calc_checksum += data[i]; + + if (rx_checksum != calc_checksum) { + ESP_LOGW(TAG, "Tuya Received invalid message checksum %02X!=%02X", rx_checksum, calc_checksum); + return false; + } + + // valid message + const uint8_t *message_data = data + 6; + ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s]", command, version, + hexencode(message_data, length).c_str()); + this->handle_command_(command, version, message_data, length); + + // return false to reset rx buffer + return false; +} + +void Tuya::handle_char_(uint8_t c) { + this->rx_message_.push_back(c); + if (!this->validate_message_()) { + this->rx_message_.clear(); + } +} + +void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { + uint8_t c; + switch ((TuyaCommandType) command) { + case TuyaCommandType::HEARTBEAT: + ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); + if (buffer[0] == 0) { + ESP_LOGI(TAG, "MCU restarted"); + this->send_empty_command_(TuyaCommandType::QUERY_STATE); + } + break; + case TuyaCommandType::QUERY_PRODUCT: { + // check it is a valid string + bool valid = false; + for (int i = 0; i < len; i++) { + if (buffer[i] == 0x00) { + valid = true; + break; + } + } + if (valid) { + ESP_LOGD(TAG, "Tuya Product Code: %s", reinterpret_cast<const char *>(buffer)); + } + break; + } + case TuyaCommandType::MCU_CONF: + if (len >= 2) { + gpio_status_ = buffer[0]; + gpio_reset_ = buffer[1]; + } + // set wifi state LED to off or on depending on the MCU firmware + // but it shouldn't be blinking + c = 0x3; + this->send_command_(TuyaCommandType::WIFI_STATE, &c, 1); + this->send_empty_command_(TuyaCommandType::QUERY_STATE); + break; + case TuyaCommandType::WIFI_STATE: + break; + case TuyaCommandType::WIFI_RESET: + ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled"); + break; + case TuyaCommandType::WIFI_SELECT: + ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled"); + break; + case TuyaCommandType::SET_DATAPOINT: + break; + case TuyaCommandType::STATE: { + this->handle_datapoint_(buffer, len); + break; + } + case TuyaCommandType::QUERY_STATE: + break; + default: + ESP_LOGE(TAG, "invalid command (%02x) received", command); + } +} + +void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { + if (len < 2) + return; + + TuyaDatapoint datapoint{}; + datapoint.id = buffer[0]; + datapoint.type = (TuyaDatapointType) buffer[1]; + datapoint.value_uint = 0; + + size_t data_size = (buffer[2] << 8) + buffer[3]; + const uint8_t *data = buffer + 4; + size_t data_len = len - 4; + if (data_size != data_len) { + ESP_LOGW(TAG, "invalid datapoint update"); + return; + } + + switch (datapoint.type) { + case TuyaDatapointType::BOOLEAN: + if (data_len != 1) + return; + datapoint.value_bool = data[0]; + break; + case TuyaDatapointType::INTEGER: + if (data_len != 4) + return; + datapoint.value_uint = + (uint32_t(data[0]) << 24) | (uint32_t(data[1]) << 16) | (uint32_t(data[2]) << 8) | (uint32_t(data[3]) << 0); + break; + case TuyaDatapointType::ENUM: + if (data_len != 1) + return; + datapoint.value_enum = data[0]; + break; + case TuyaDatapointType::BITMASK: + if (data_len != 2) + return; + datapoint.value_bitmask = (uint16_t(data[0]) << 8) | (uint16_t(data[1]) << 0); + break; + default: + return; + } + ESP_LOGV(TAG, "Datapoint %u update to %u", datapoint.id, datapoint.value_uint); + + // Update internal datapoints + bool found = false; + for (auto &other : this->datapoints_) { + if (other.id == datapoint.id) { + other = datapoint; + found = true; + } + } + if (!found) { + this->datapoints_.push_back(datapoint); + // New datapoint found, reprint dump_config after a delay. + this->set_timeout("datapoint_dump", 100, [this] { this->dump_config(); }); + } + + // Run through listeners + for (auto &listener : this->listeners_) + if (listener.datapoint_id == datapoint.id) + listener.on_datapoint(datapoint); +} + +void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) { + uint8_t len_hi = len >> 8; + uint8_t len_lo = len >> 0; + this->write_array({0x55, 0xAA, + 0x00, // version + (uint8_t) command, len_hi, len_lo}); + if (len != 0) + this->write_array(buffer, len); + + uint8_t checksum = 0x55 + 0xAA + (uint8_t) command + len_hi + len_lo; + for (int i = 0; i < len; i++) + checksum += buffer[i]; + this->write_byte(checksum); +} + +void Tuya::set_datapoint_value(TuyaDatapoint datapoint) { + std::vector<uint8_t> buffer; + ESP_LOGV(TAG, "Datapoint %u set to %u", datapoint.id, datapoint.value_uint); + for (auto &other : this->datapoints_) { + if (other.id == datapoint.id) { + if (other.value_uint == datapoint.value_uint) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + } + } + buffer.push_back(datapoint.id); + buffer.push_back(static_cast<uint8_t>(datapoint.type)); + + std::vector<uint8_t> data; + switch (datapoint.type) { + case TuyaDatapointType::BOOLEAN: + data.push_back(datapoint.value_bool); + break; + case TuyaDatapointType::INTEGER: + data.push_back(datapoint.value_uint >> 24); + data.push_back(datapoint.value_uint >> 16); + data.push_back(datapoint.value_uint >> 8); + data.push_back(datapoint.value_uint >> 0); + break; + case TuyaDatapointType::ENUM: + data.push_back(datapoint.value_enum); + break; + case TuyaDatapointType::BITMASK: + data.push_back(datapoint.value_bitmask >> 8); + data.push_back(datapoint.value_bitmask >> 0); + break; + default: + return; + } + + buffer.push_back(data.size() >> 8); + buffer.push_back(data.size() >> 0); + buffer.insert(buffer.end(), data.begin(), data.end()); + this->send_command_(TuyaCommandType::SET_DATAPOINT, buffer.data(), buffer.size()); +} + +void Tuya::register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func) { + auto listener = TuyaDatapointListener{ + .datapoint_id = datapoint_id, + .on_datapoint = func, + }; + this->listeners_.push_back(listener); + + // Run through existing datapoints + for (auto &datapoint : this->datapoints_) + if (datapoint.id == datapoint_id) + func(datapoint); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h new file mode 100644 index 0000000000..6bc6d92da0 --- /dev/null +++ b/esphome/components/tuya/tuya.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace tuya { + +enum class TuyaDatapointType : uint8_t { + RAW = 0x00, // variable length + BOOLEAN = 0x01, // 1 byte (0/1) + INTEGER = 0x02, // 4 byte + STRING = 0x03, // variable length + ENUM = 0x04, // 1 byte + BITMASK = 0x05, // 2 bytes +}; + +struct TuyaDatapoint { + uint8_t id; + TuyaDatapointType type; + union { + bool value_bool; + int value_int; + uint32_t value_uint; + uint8_t value_enum; + uint16_t value_bitmask; + }; +}; + +struct TuyaDatapointListener { + uint8_t datapoint_id; + std::function<void(TuyaDatapoint)> on_datapoint; +}; + +enum class TuyaCommandType : uint8_t { + HEARTBEAT = 0x00, + QUERY_PRODUCT = 0x01, + MCU_CONF = 0x02, + WIFI_STATE = 0x03, + WIFI_RESET = 0x04, + WIFI_SELECT = 0x05, + SET_DATAPOINT = 0x06, + STATE = 0x07, + QUERY_STATE = 0x08, +}; + +class Tuya : public Component, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void setup() override; + void loop() override; + void dump_config() override; + void register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func); + void set_datapoint_value(TuyaDatapoint datapoint); + + protected: + void handle_char_(uint8_t c); + void handle_datapoint_(const uint8_t *buffer, size_t len); + bool validate_message_(); + + void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len); + void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); + void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } + + int gpio_status_ = -1; + int gpio_reset_ = -1; + std::vector<TuyaDatapointListener> listeners_; + std::vector<TuyaDatapoint> datapoints_; + std::vector<uint8_t> rx_message_; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tx20/__init__.py b/esphome/components/tx20/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py new file mode 100644 index 0000000000..daa6677196 --- /dev/null +++ b/esphome/components/tx20/sensor.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_WIND_SPEED, CONF_PIN, \ + CONF_WIND_DIRECTION_DEGREES, UNIT_KILOMETER_PER_HOUR, \ + UNIT_EMPTY, ICON_WEATHER_WINDY, ICON_SIGN_DIRECTION + +tx20_ns = cg.esphome_ns.namespace('tx20') +Tx20Component = tx20_ns.class_('Tx20Component', cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Tx20Component), + cv.Optional(CONF_WIND_SPEED): + sensor.sensor_schema(UNIT_KILOMETER_PER_HOUR, ICON_WEATHER_WINDY, 1), + cv.Optional(CONF_WIND_DIRECTION_DEGREES): + sensor.sensor_schema(UNIT_EMPTY, ICON_SIGN_DIRECTION, 1), + cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema, + pins.validate_has_interrupt), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + if CONF_WIND_SPEED in config: + conf = config[CONF_WIND_SPEED] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_wind_speed_sensor(sens)) + + if CONF_WIND_DIRECTION_DEGREES in config: + conf = config[CONF_WIND_DIRECTION_DEGREES] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_wind_direction_degrees_sensor(sens)) + + pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp new file mode 100644 index 0000000000..f3dafda288 --- /dev/null +++ b/esphome/components/tx20/tx20.cpp @@ -0,0 +1,195 @@ +#include "tx20.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tx20 { + +static const char *TAG = "tx20"; +static const uint8_t MAX_BUFFER_SIZE = 41; +static const uint16_t TX20_MAX_TIME = MAX_BUFFER_SIZE * 1200 + 5000; +static const uint16_t TX20_BIT_TIME = 1200; +static const char *DIRECTIONS[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", + "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"}; + +void Tx20Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up Tx20"); + this->pin_->setup(); + + this->store_.buffer = new uint16_t[MAX_BUFFER_SIZE]; + this->store_.pin = this->pin_->to_isr(); + this->store_.reset(); + + this->pin_->attach_interrupt(Tx20ComponentStore::gpio_intr, &this->store_, CHANGE); +} +void Tx20Component::dump_config() { + ESP_LOGCONFIG(TAG, "Tx20:"); + + LOG_SENSOR(" ", "Wind speed:", this->wind_speed_sensor_); + LOG_SENSOR(" ", "Wind direction degrees:", this->wind_direction_degrees_sensor_); + + LOG_PIN(" Pin: ", this->pin_); +} +void Tx20Component::loop() { + if (this->store_.tx20_available) { + this->decode_and_publish_(); + this->store_.reset(); + } +} + +float Tx20Component::get_setup_priority() const { return setup_priority::DATA; } + +std::string Tx20Component::get_wind_cardinal_direction() const { return this->wind_cardinal_direction_; } + +void Tx20Component::decode_and_publish_() { + ESP_LOGVV(TAG, "Decode Tx20..."); + + std::string string_buffer; + std::string string_buffer_2; + std::vector<bool> bit_buffer; + bool current_bit = true; + + for (int i = 1; i <= this->store_.buffer_index; i++) { + string_buffer_2 += to_string(this->store_.buffer[i]) + ", "; + uint8_t repeat = this->store_.buffer[i] / TX20_BIT_TIME; + // ignore segments at the end that were too short + string_buffer.append(repeat, current_bit ? '1' : '0'); + bit_buffer.insert(bit_buffer.end(), repeat, current_bit); + current_bit = !current_bit; + } + current_bit = !current_bit; + if (string_buffer.length() < MAX_BUFFER_SIZE) { + uint8_t remain = MAX_BUFFER_SIZE - string_buffer.length(); + string_buffer_2 += to_string(remain) + ", "; + string_buffer.append(remain, current_bit ? '1' : '0'); + bit_buffer.insert(bit_buffer.end(), remain, current_bit); + } + + uint8_t tx20_sa = 0; + uint8_t tx20_sb = 0; + uint8_t tx20_sd = 0; + uint8_t tx20_se = 0; + uint16_t tx20_sc = 0; + uint16_t tx20_sf = 0; + uint8_t tx20_wind_direction = 0; + float tx20_wind_speed_kmh = 0; + uint8_t bit_count = 0; + + for (int i = 41; i > 0; i--) { + uint8_t bit = bit_buffer.at(bit_count); + bit_count++; + if (i > 41 - 5) { + // start, inverted + tx20_sa = (tx20_sa << 1) | (bit ^ 1); + } else if (i > 41 - 5 - 4) { + // wind dir, inverted + tx20_sb = tx20_sb >> 1 | ((bit ^ 1) << 3); + } else if (i > 41 - 5 - 4 - 12) { + // windspeed, inverted + tx20_sc = tx20_sc >> 1 | ((bit ^ 1) << 11); + } else if (i > 41 - 5 - 4 - 12 - 4) { + // checksum, inverted + tx20_sd = tx20_sd >> 1 | ((bit ^ 1) << 3); + } else if (i > 41 - 5 - 4 - 12 - 4 - 4) { + // wind dir + tx20_se = tx20_se >> 1 | (bit << 3); + } else { + // windspeed + tx20_sf = tx20_sf >> 1 | (bit << 11); + } + } + + uint8_t chk = (tx20_sb + (tx20_sc & 0xf) + ((tx20_sc >> 4) & 0xf) + ((tx20_sc >> 8) & 0xf)); + chk &= 0xf; + bool value_set = false; + // checks: + // 1. Check that the start frame is 00100 (0x04) + // 2. Check received checksum matches calculated checksum + // 3. Check that Wind Direction matches Wind Direction (Inverted) + // 4. Check that Wind Speed matches Wind Speed (Inverted) + ESP_LOGVV(TAG, "BUFFER %s", string_buffer_2.c_str()); + ESP_LOGVV(TAG, "Decoded bits %s", string_buffer.c_str()); + + if (tx20_sa == 4) { + if (chk == tx20_sd) { + if (tx20_sf == tx20_sc) { + tx20_wind_speed_kmh = float(tx20_sc) * 0.36; + ESP_LOGV(TAG, "WindSpeed %f", tx20_wind_speed_kmh); + if (this->wind_speed_sensor_ != nullptr) + this->wind_speed_sensor_->publish_state(tx20_wind_speed_kmh); + value_set = true; + } + if (tx20_se == tx20_sb) { + tx20_wind_direction = tx20_se; + if (tx20_wind_direction >= 0 && tx20_wind_direction < 16) { + wind_cardinal_direction_ = DIRECTIONS[tx20_wind_direction]; + } + ESP_LOGV(TAG, "WindDirection %d", tx20_wind_direction); + if (this->wind_direction_degrees_sensor_ != nullptr) + this->wind_direction_degrees_sensor_->publish_state(float(tx20_wind_direction) * 22.5f); + value_set = true; + } + if (!value_set) { + ESP_LOGW(TAG, "No value set!"); + } + } else { + ESP_LOGW(TAG, "Checksum wrong!"); + } + } else { + ESP_LOGW(TAG, "Start wrong!"); + } +} + +void ICACHE_RAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { + arg->pin_state = arg->pin->digital_read(); + const uint32_t now = micros(); + if (!arg->start_time) { + // only detect a start if the bit is high + if (!arg->pin_state) { + return; + } + arg->buffer[arg->buffer_index] = 1; + arg->start_time = now; + arg->buffer_index++; + return; + } + const uint32_t delay = now - arg->start_time; + const uint8_t index = arg->buffer_index; + + // first delay has to be ~2400 + if (index == 1 && (delay > 3000 || delay < 2400)) { + arg->reset(); + return; + } + // second delay has to be ~1200 + if (index == 2 && (delay > 1500 || delay < 1200)) { + arg->reset(); + return; + } + // third delay has to be ~2400 + if (index == 3 && (delay > 3000 || delay < 2400)) { + arg->reset(); + return; + } + + if (arg->tx20_available || ((arg->spent_time + delay > TX20_MAX_TIME) && arg->start_time)) { + arg->tx20_available = true; + return; + } + if (index <= MAX_BUFFER_SIZE) { + arg->buffer[index] = delay; + } + arg->spent_time += delay; + arg->start_time = now; + arg->buffer_index++; +} +void ICACHE_RAM_ATTR Tx20ComponentStore::reset() { + tx20_available = false; + buffer_index = 0; + spent_time = 0; + // rearm it! + start_time = 0; +} + +} // namespace tx20 +} // namespace esphome diff --git a/esphome/components/tx20/tx20.h b/esphome/components/tx20/tx20.h new file mode 100644 index 0000000000..8b79deffbc --- /dev/null +++ b/esphome/components/tx20/tx20.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace tx20 { + +/// Store data in a class that doesn't use multiple-inheritance (vtables in flash) +struct Tx20ComponentStore { + volatile uint16_t *buffer; + volatile uint32_t start_time; + volatile uint8_t buffer_index; + volatile uint32_t spent_time; + volatile bool tx20_available; + volatile bool pin_state; + ISRInternalGPIOPin *pin; + + void reset(); + static void gpio_intr(Tx20ComponentStore *arg); +}; + +/// This class implements support for the Tx20 Wind sensor. +class Tx20Component : public Component { + public: + /// Get the textual representation of the wind direction ('N', 'SSE', ..). + std::string get_wind_cardinal_direction() const; + + void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_wind_speed_sensor(sensor::Sensor *wind_speed_sensor) { wind_speed_sensor_ = wind_speed_sensor; } + void set_wind_direction_degrees_sensor(sensor::Sensor *wind_direction_degrees_sensor) { + wind_direction_degrees_sensor_ = wind_direction_degrees_sensor; + } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + protected: + void decode_and_publish_(); + + std::string wind_cardinal_direction_; + GPIOPin *pin_; + sensor::Sensor *wind_speed_sensor_; + sensor::Sensor *wind_direction_degrees_sensor_; + Tx20ComponentStore store_; +}; + +} // namespace tx20 +} // namespace esphome diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index c374568149..2511cf28b1 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -29,15 +29,18 @@ def validate_rx_pin(value): return value +CONF_STOP_BITS = 'stop_bits' CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(UARTComponent), - cv.Required(CONF_BAUD_RATE): cv.int_range(min=1, max=115200), + cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_TX_PIN): pins.output_pin, cv.Optional(CONF_RX_PIN): validate_rx_pin, + cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), }).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN)) def to_code(config): + cg.add_global(uart_ns.using) var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) @@ -47,6 +50,7 @@ def to_code(config): cg.add(var.set_tx_pin(config[CONF_TX_PIN])) if CONF_RX_PIN in config: cg.add(var.set_rx_pin(config[CONF_RX_PIN])) + cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) # A schema to use for all UART devices, all UART integrations must extend this! diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 56661b8aa7..3284d4cb67 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -2,6 +2,11 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif namespace esphome { namespace uart { @@ -20,16 +25,15 @@ void UARTComponent::setup() { // is 1 we still want to use Serial. if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { this->hw_serial_ = &Serial; - } else if (this->tx_pin_.value_or(9) == 9 && this->rx_pin_.value_or(10) == 10) { - this->hw_serial_ = &Serial1; - } else if (this->tx_pin_.value_or(16) == 16 && this->rx_pin_.value_or(17) == 17) { - this->hw_serial_ = &Serial2; } else { this->hw_serial_ = new HardwareSerial(next_uart_num++); } int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->hw_serial_->begin(this->baud_rate_, SERIAL_8N1, rx, tx); + uint32_t config = SERIAL_8N1; + if (this->stop_bits_ == 2) + config = SERIAL_8N2; + this->hw_serial_->begin(this->baud_rate_, config, rx, tx); } void UARTComponent::dump_config() { @@ -41,6 +45,13 @@ void UARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); +#ifdef USE_LOGGER + if (this->hw_serial_ == &Serial && logger::global_logger->get_baud_rate() != 0) { + ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " + "disable logging over the serial port by setting logger->baud_rate to 0."); + } +#endif } void UARTComponent::write_byte(uint8_t data) { @@ -106,21 +117,27 @@ void UARTComponent::setup() { // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. + uint32_t mode = UART_NB_BIT_8 | UART_PARITY_NONE; + if (this->stop_bits_ == 1) + mode |= UART_NB_STOP_BIT_1; + else + mode |= UART_NB_STOP_BIT_2; + SerialConfig config = static_cast<SerialConfig>(mode); if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->swap(); } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { this->hw_serial_ = &Serial1; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); } else { this->sw_serial_ = new ESP8266SoftwareSerial(); int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->sw_serial_->setup(tx, rx, this->baud_rate_); + this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_); } } @@ -133,11 +150,19 @@ void UARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); if (this->hw_serial_ != nullptr) { ESP_LOGCONFIG(TAG, " Using hardware serial interface."); } else { ESP_LOGCONFIG(TAG, " Using software serial"); } + +#ifdef USE_LOGGER + if (this->hw_serial_ == &Serial && logger::global_logger->get_baud_rate() != 0) { + ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " + "disable logging over the serial port by setting logger->baud_rate to 0."); + } +#endif } void UARTComponent::write_byte(uint8_t data) { @@ -235,7 +260,7 @@ void UARTComponent::flush() { } } -void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate) { +void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits) { this->bit_time_ = F_CPU / baud_rate; if (tx_pin != -1) { auto pin = GPIOPin(tx_pin, OUTPUT); @@ -250,6 +275,7 @@ void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_ra this->rx_buffer_ = new uint8_t[this->rx_buffer_size_]; pin.attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); } + this->stop_bits_ = stop_bits; } void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500; @@ -266,6 +292,8 @@ void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg rec |= arg->read_bit_(&wait, start) << 7; // Stop bit arg->wait_(&wait, start); + if (arg->stop_bits_ == 2) + arg->wait_(&wait, start); arg->rx_buffer_[arg->rx_in_pos_] = rec; arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_; @@ -293,14 +321,16 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { this->write_bit_(data & (1 << 7), &wait, start); // Stop bit this->write_bit_(true, &wait, start); + if (this->stop_bits_ == 2) + this->wait_(&wait, start); enable_interrupts(); } -void ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { +void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { while (ESP.getCycleCount() - start < *wait) ; *wait += this->bit_time_; } -bool ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { +bool ICACHE_RAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { this->wait_(wait, start); return this->rx_pin_->digital_read(); } @@ -320,7 +350,9 @@ uint8_t ESP8266SoftwareSerial::peek_byte() { return 0; return this->rx_buffer_[this->rx_out_pos_]; } -void ESP8266SoftwareSerial::flush() { this->rx_in_pos_ = this->rx_out_pos_ = 0; } +void ESP8266SoftwareSerial::flush() { + // Flush is a NO-OP with software serial, all bytes are written immediately. +} int ESP8266SoftwareSerial::available() { int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); if (avail < 0) @@ -346,5 +378,16 @@ int UARTComponent::peek() { return data; } +void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits) { + if (this->parent_->baud_rate_ != baud_rate) { + ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate, + this->parent_->baud_rate_); + } + if (this->parent_->stop_bits_ != stop_bits) { + ESP_LOGE(TAG, " Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits, + this->parent_->stop_bits_); + } +} + } // namespace uart } // namespace esphome diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 666b8e2fb2..0e92fed0dc 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -10,7 +10,7 @@ namespace uart { #ifdef ARDUINO_ARCH_ESP8266 class ESP8266SoftwareSerial { public: - void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate); + void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits); uint8_t read_byte(); uint8_t peek_byte(); @@ -24,15 +24,16 @@ class ESP8266SoftwareSerial { protected: static void gpio_intr(ESP8266SoftwareSerial *arg); - inline void wait_(uint32_t *wait, const uint32_t &start); - inline bool read_bit_(uint32_t *wait, const uint32_t &start); - inline void write_bit_(bool bit, uint32_t *wait, const uint32_t &start); + void wait_(uint32_t *wait, const uint32_t &start); + bool read_bit_(uint32_t *wait, const uint32_t &start); + void write_bit_(bool bit, uint32_t *wait, const uint32_t &start); uint32_t bit_time_{0}; uint8_t *rx_buffer_{nullptr}; - size_t rx_buffer_size_{64}; + size_t rx_buffer_size_{512}; volatile size_t rx_in_pos_{0}; size_t rx_out_pos_{0}; + uint8_t stop_bits_; ISRInternalGPIOPin *tx_pin_{nullptr}; ISRInternalGPIOPin *rx_pin_{nullptr}; }; @@ -61,6 +62,7 @@ class UARTComponent : public Component, public Stream { int available() override; + /// Block until all bytes have been written to the UART bus. void flush() override; float get_setup_priority() const override { return setup_priority::BUS; } @@ -71,9 +73,11 @@ class UARTComponent : public Component, public Stream { void set_tx_pin(uint8_t tx_pin) { this->tx_pin_ = tx_pin; } void set_rx_pin(uint8_t rx_pin) { this->rx_pin_ = rx_pin; } + void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } protected: bool check_read_timeout_(size_t len = 1); + friend class UARTDevice; HardwareSerial *hw_serial_{nullptr}; #ifdef ARDUINO_ARCH_ESP8266 @@ -82,6 +86,7 @@ class UARTComponent : public Component, public Stream { optional<uint8_t> tx_pin_; optional<uint8_t> rx_pin_; uint32_t baud_rate_; + uint8_t stop_bits_; }; #ifdef ARDUINO_ARCH_ESP32 @@ -99,6 +104,9 @@ class UARTDevice : public Stream { void write_array(const uint8_t *data, size_t len) { this->parent_->write_array(data, len); } void write_array(const std::vector<uint8_t> &data) { this->parent_->write_array(data); } + template<size_t N> void write_array(const std::array<uint8_t, N> &data) { + this->parent_->write_array(data.data(), data.size()); + } void write_str(const char *str) { this->parent_->write_str(str); } @@ -106,6 +114,13 @@ class UARTDevice : public Stream { bool peek_byte(uint8_t *data) { return this->parent_->peek_byte(data); } bool read_array(uint8_t *data, size_t len) { return this->parent_->read_array(data, len); } + template<size_t N> optional<std::array<uint8_t, N>> read_array() { // NOLINT + std::array<uint8_t, N> res; + if (!this->read_array(res.data(), N)) { + return {}; + } + return res; + } int available() override { return this->parent_->available(); } @@ -115,6 +130,9 @@ class UARTDevice : public Stream { int read() override { return this->parent_->read(); } int peek() override { return this->parent_->peek(); } + /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning + void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1); + protected: UARTComponent *parent_{nullptr}; }; diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index 5d4cd48bc1..f8130f7d1f 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -26,7 +26,6 @@ void UltrasonicSensorComponent::update() { this->publish_state(NAN); } else { float result = UltrasonicSensorComponent::us_to_m(time); - this->publish_state(result); ESP_LOGD(TAG, "'%s' - Got distance: %.2f m", this->name_.c_str(), result); this->publish_state(result); } diff --git a/esphome/components/uptime/uptime_sensor.cpp b/esphome/components/uptime/uptime_sensor.cpp index f047724768..5d117ab61d 100644 --- a/esphome/components/uptime/uptime_sensor.cpp +++ b/esphome/components/uptime/uptime_sensor.cpp @@ -27,6 +27,7 @@ void UptimeSensor::update() { } std::string UptimeSensor::unique_id() { return get_mac_address() + "-uptime"; } float UptimeSensor::get_setup_priority() const { return setup_priority::HARDWARE; } +void UptimeSensor::dump_config() { LOG_SENSOR("", "Uptime Sensor", this); } } // namespace uptime } // namespace esphome diff --git a/esphome/components/uptime/uptime_sensor.h b/esphome/components/uptime/uptime_sensor.h index 184022503d..dab380d2d9 100644 --- a/esphome/components/uptime/uptime_sensor.h +++ b/esphome/components/uptime/uptime_sensor.h @@ -9,6 +9,7 @@ namespace uptime { class UptimeSensor : public sensor::Sensor, public PollingComponent { public: void update() override; + void dump_config() override; float get_setup_priority() const override; diff --git a/esphome/components/vl53l0x/LICENSE.txt b/esphome/components/vl53l0x/LICENSE.txt new file mode 100644 index 0000000000..fe33583414 --- /dev/null +++ b/esphome/components/vl53l0x/LICENSE.txt @@ -0,0 +1,80 @@ +Most of the code in this integration is based on the VL53L0x library +by Pololu (Pololu Corporation), which in turn is based on the VL53L0X +API from ST. The code has been adapted to work with ESPHome's i2c APIs. +Please see the top-level LICENSE.txt for information about ESPHome's license. +The licenses for Pololu's and ST's software are included below. +Orignally taken from https://github.com/pololu/vl53l0x-arduino (accessed 20th october 2019). + +================================================================= + +Copyright (c) 2017 Pololu Corporation. For more information, see + +https://www.pololu.com/ +https://forum.pololu.com/ + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +================================================================= + +Most of the functionality of this library is based on the VL53L0X +API provided by ST (STSW-IMG005), and some of the explanatory +comments are quoted or paraphrased from the API source code, API +user manual (UM2039), and the VL53L0X datasheet. + +The following applies to source code reproduced or derived from +the API: + +----------------------------------------------------------------- + +Copyright © 2016, STMicroelectronics International N.V. All +rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the following +conditions are met: +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided +with the distribution. +* Neither the name of STMicroelectronics nor the +names of its contributors may be used to endorse or promote +products derived from this software without specific prior +written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND +NON-INFRINGEMENT OF INTELLECTUAL PROPERTY RIGHTS ARE DISCLAIMED. +IN NO EVENT SHALL STMICROELECTRONICS INTERNATIONAL N.V. BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +----------------------------------------------------------------- diff --git a/esphome/components/vl53l0x/__init__.py b/esphome/components/vl53l0x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py new file mode 100644 index 0000000000..6740d53e13 --- /dev/null +++ b/esphome/components/vl53l0x/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL + +DEPENDENCIES = ['i2c'] + +vl53l0x_ns = cg.esphome_ns.namespace('vl53l0x') +VL53L0XSensor = vl53l0x_ns.class_('VL53L0XSensor', sensor.Sensor, cg.PollingComponent, + i2c.I2CDevice) + +CONF_SIGNAL_RATE_LIMIT = 'signal_rate_limit' +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2).extend({ + cv.GenerateID(): cv.declare_id(VL53L0XSensor), + cv.Optional(CONF_SIGNAL_RATE_LIMIT, default=0.25): cv.float_range( + min=0.0, max=512.0, min_included=False, max_included=False) +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x29)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp new file mode 100644 index 0000000000..231bed99ac --- /dev/null +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -0,0 +1,249 @@ +#include "vl53l0x_sensor.h" +#include "esphome/core/log.h" + +/* + * Most of the code in this integration is based on the VL53L0x library + * by Pololu (Pololu Corporation), which in turn is based on the VL53L0X + * API from ST. + * + * For more information about licensing, please view the included LICENSE.txt file + * in the vl53l0x integration directory. + */ + +namespace esphome { +namespace vl53l0x { + +static const char *TAG = "vl53l0x"; + +void VL53L0XSensor::dump_config() { + LOG_SENSOR("", "VL53L0X", this); + LOG_UPDATE_INTERVAL(this); + LOG_I2C_DEVICE(this); +} +void VL53L0XSensor::setup() { + reg(0x89) |= 0x01; + reg(0x88) = 0x00; + + reg(0x80) = 0x01; + reg(0xFF) = 0x01; + reg(0x00) = 0x00; + stop_variable_ = reg(0x91).get(); + + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + reg(0x60) |= 0x12; + + auto rate_value = static_cast<uint16_t>(signal_rate_limit_ * 128); + write_byte_16(0x44, rate_value); + + reg(0x01) = 0xFF; + + // getSpadInfo() + reg(0x80) = 0x01; + reg(0xFF) = 0x01; + reg(0x00) = 0x00; + reg(0xFF) = 0x06; + reg(0x83) |= 0x04; + reg(0xFF) = 0x07; + reg(0x81) = 0x01; + reg(0x80) = 0x01; + reg(0x94) = 0x6B; + reg(0x83) = 0x00; + + while (reg(0x83).get() == 0x00) + yield(); + + reg(0x83) = 0x01; + uint8_t tmp = reg(0x92).get(); + uint8_t spad_count = tmp & 0x7F; + bool spad_type_is_aperture = tmp & 0x80; + + reg(0x81) = 0x00; + reg(0xFF) = 0x06; + reg(0x83) &= ~0x04; + reg(0xFF) = 0x01; + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + + uint8_t ref_spad_map[6]; + this->read_bytes(0xB0, ref_spad_map, 6); + + reg(0xFF) = 0x01; + reg(0x4F) = 0x00; + reg(0x4E) = 0x2C; + reg(0xFF) = 0x00; + reg(0xB6) = 0xB4; + + uint8_t first_spad_to_enable = spad_type_is_aperture ? 12 : 0; + uint8_t spads_enabled = 0; + for (int i = 0; i < 48; i++) { + uint8_t &val = ref_spad_map[i / 8]; + uint8_t mask = 1 << (i % 8); + + if (i < first_spad_to_enable || spads_enabled == spad_count) + val &= ~mask; + else if (val & mask) + spads_enabled += 1; + } + + this->write_bytes(0xB0, ref_spad_map, 6); + + reg(0xFF) = 0x01; + reg(0x00) = 0x00; + reg(0xFF) = 0x00; + reg(0x09) = 0x00; + reg(0x10) = 0x00; + reg(0x11) = 0x00; + reg(0x24) = 0x01; + reg(0x25) = 0xFF; + reg(0x75) = 0x00; + reg(0xFF) = 0x01; + reg(0x4E) = 0x2C; + reg(0x48) = 0x00; + reg(0x30) = 0x20; + reg(0xFF) = 0x00; + reg(0x30) = 0x09; + reg(0x54) = 0x00; + reg(0x31) = 0x04; + reg(0x32) = 0x03; + reg(0x40) = 0x83; + reg(0x46) = 0x25; + reg(0x60) = 0x00; + reg(0x27) = 0x00; + reg(0x50) = 0x06; + reg(0x51) = 0x00; + reg(0x52) = 0x96; + reg(0x56) = 0x08; + reg(0x57) = 0x30; + reg(0x61) = 0x00; + reg(0x62) = 0x00; + reg(0x64) = 0x00; + reg(0x65) = 0x00; + reg(0x66) = 0xA0; + reg(0xFF) = 0x01; + reg(0x22) = 0x32; + reg(0x47) = 0x14; + reg(0x49) = 0xFF; + reg(0x4A) = 0x00; + reg(0xFF) = 0x00; + reg(0x7A) = 0x0A; + reg(0x7B) = 0x00; + reg(0x78) = 0x21; + reg(0xFF) = 0x01; + reg(0x23) = 0x34; + reg(0x42) = 0x00; + reg(0x44) = 0xFF; + reg(0x45) = 0x26; + reg(0x46) = 0x05; + reg(0x40) = 0x40; + reg(0x0E) = 0x06; + reg(0x20) = 0x1A; + reg(0x43) = 0x40; + reg(0xFF) = 0x00; + reg(0x34) = 0x03; + reg(0x35) = 0x44; + reg(0xFF) = 0x01; + reg(0x31) = 0x04; + reg(0x4B) = 0x09; + reg(0x4C) = 0x05; + reg(0x4D) = 0x04; + reg(0xFF) = 0x00; + reg(0x44) = 0x00; + reg(0x45) = 0x20; + reg(0x47) = 0x08; + reg(0x48) = 0x28; + reg(0x67) = 0x00; + reg(0x70) = 0x04; + reg(0x71) = 0x01; + reg(0x72) = 0xFE; + reg(0x76) = 0x00; + reg(0x77) = 0x00; + reg(0xFF) = 0x01; + reg(0x0D) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x01; + reg(0x01) = 0xF8; + reg(0xFF) = 0x01; + reg(0x8E) = 0x01; + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + + reg(0x0A) = 0x04; + reg(0x84) &= ~0x10; + reg(0x0B) = 0x01; + + measurement_timing_budget_us_ = get_measurement_timing_budget_(); + reg(0x01) = 0xE8; + set_measurement_timing_budget_(measurement_timing_budget_us_); + reg(0x01) = 0x01; + + if (!perform_single_ref_calibration_(0x40)) { + ESP_LOGW(TAG, "1st reference calibration failed!"); + this->mark_failed(); + return; + } + reg(0x01) = 0x02; + if (!perform_single_ref_calibration_(0x00)) { + ESP_LOGW(TAG, "2nd reference calibration failed!"); + this->mark_failed(); + return; + } + reg(0x01) = 0xE8; +} +void VL53L0XSensor::update() { + if (this->initiated_read_ || this->waiting_for_interrupt_) { + this->publish_state(NAN); + this->status_set_warning(); + } + + // initiate single shot measurement + reg(0x80) = 0x01; + reg(0xFF) = 0x01; + + reg(0x00) = 0x00; + reg(0x91) = stop_variable_; + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + + reg(0x00) = 0x01; + this->waiting_for_interrupt_ = false; + this->initiated_read_ = true; + // wait for timeout +} +void VL53L0XSensor::loop() { + if (this->initiated_read_) { + if (reg(0x00).get() & 0x01) { + // waiting + } else { + // done + // wait until reg(0x13) & 0x07 is set + this->initiated_read_ = false; + this->waiting_for_interrupt_ = true; + } + } + if (this->waiting_for_interrupt_) { + if (reg(0x13).get() & 0x07) { + uint16_t range_mm; + this->read_byte_16(0x14 + 10, &range_mm); + reg(0x0B) = 0x01; + this->waiting_for_interrupt_ = false; + + if (range_mm >= 8190) { + ESP_LOGW(TAG, "'%s' - Distance is out of range, please move the target closer", this->name_.c_str()); + this->publish_state(NAN); + return; + } + + float range_m = range_mm / 1e3f; + ESP_LOGD(TAG, "'%s' - Got distance %.3f m", this->name_.c_str(), range_m); + this->publish_state(range_m); + } + } +} + +} // namespace vl53l0x +} // namespace esphome diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h new file mode 100644 index 0000000000..1825383cee --- /dev/null +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -0,0 +1,257 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace vl53l0x { + +struct SequenceStepEnables { + bool tcc, msrc, dss, pre_range, final_range; +}; + +struct SequenceStepTimeouts { + uint16_t pre_range_vcsel_period_pclks, final_range_vcsel_period_pclks; + + uint16_t msrc_dss_tcc_mclks, pre_range_mclks, final_range_mclks; + uint32_t msrc_dss_tcc_us, pre_range_us, final_range_us; +}; + +class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void update() override; + + void loop() override; + + void set_signal_rate_limit(float signal_rate_limit) { signal_rate_limit_ = signal_rate_limit; } + + protected: + uint32_t get_measurement_timing_budget_() { + SequenceStepEnables enables{}; + SequenceStepTimeouts timeouts{}; + + uint16_t start_overhead = 1910; + uint16_t end_overhead = 960; + uint16_t msrc_overhead = 660; + uint16_t tcc_overhead = 590; + uint16_t dss_overhead = 690; + uint16_t pre_range_overhead = 660; + uint16_t final_range_overhead = 550; + + // "Start and end overhead times always present" + uint32_t budget_us = start_overhead + end_overhead; + + get_sequence_step_enables_(&enables); + get_sequence_step_timeouts_(&enables, &timeouts); + + if (enables.tcc) + budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead); + + if (enables.dss) + budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead); + else if (enables.msrc) + budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead); + + if (enables.pre_range) + budget_us += (timeouts.pre_range_us + pre_range_overhead); + + if (enables.final_range) + budget_us += (timeouts.final_range_us + final_range_overhead); + + measurement_timing_budget_us_ = budget_us; // store for internal reuse + return budget_us; + } + + bool set_measurement_timing_budget_(uint32_t budget_us) { + SequenceStepEnables enables{}; + SequenceStepTimeouts timeouts{}; + + uint16_t start_overhead = 1320; // note that this is different than the value in get_ + uint16_t end_overhead = 960; + uint16_t msrc_overhead = 660; + uint16_t tcc_overhead = 590; + uint16_t dss_overhead = 690; + uint16_t pre_range_overhead = 660; + uint16_t final_range_overhead = 550; + + uint32_t min_timing_budget = 20000; + + if (budget_us < min_timing_budget) { + return false; + } + + uint32_t used_budget_us = start_overhead + end_overhead; + + get_sequence_step_enables_(&enables); + get_sequence_step_timeouts_(&enables, &timeouts); + + if (enables.tcc) { + used_budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead); + } + + if (enables.dss) { + used_budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead); + } else if (enables.msrc) { + used_budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead); + } + + if (enables.pre_range) { + used_budget_us += (timeouts.pre_range_us + pre_range_overhead); + } + + if (enables.final_range) { + used_budget_us += final_range_overhead; + + // "Note that the final range timeout is determined by the timing + // budget and the sum of all other timeouts within the sequence. + // If there is no room for the final range timeout, then an error + // will be set. Otherwise the remaining time will be applied to + // the final range." + + if (used_budget_us > budget_us) { + // "Requested timeout too big." + return false; + } + + uint32_t final_range_timeout_us = budget_us - used_budget_us; + + // set_sequence_step_timeout() begin + // (SequenceStepId == VL53L0X_SEQUENCESTEP_FINAL_RANGE) + + // "For the final range timeout, the pre-range timeout + // must be added. To do this both final and pre-range + // timeouts must be expressed in macro periods MClks + // because they have different vcsel periods." + + uint16_t final_range_timeout_mclks = + timeout_microseconds_to_mclks_(final_range_timeout_us, timeouts.final_range_vcsel_period_pclks); + + if (enables.pre_range) { + final_range_timeout_mclks += timeouts.pre_range_mclks; + } + + write_byte_16(0x71, encode_timeout_(final_range_timeout_mclks)); + + // set_sequence_step_timeout() end + + measurement_timing_budget_us_ = budget_us; // store for internal reuse + } + return true; + } + + void get_sequence_step_enables_(SequenceStepEnables *enables) { + uint8_t sequence_config = reg(0x01).get(); + enables->tcc = (sequence_config >> 4) & 0x1; + enables->dss = (sequence_config >> 3) & 0x1; + enables->msrc = (sequence_config >> 2) & 0x1; + enables->pre_range = (sequence_config >> 6) & 0x1; + enables->final_range = (sequence_config >> 7) & 0x1; + } + + enum VcselPeriodType { VCSEL_PERIOD_PRE_RANGE, VCSEL_PERIOD_FINAL_RANGE }; + + void get_sequence_step_timeouts_(SequenceStepEnables const *enables, SequenceStepTimeouts *timeouts) { + timeouts->pre_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_PRE_RANGE); + + timeouts->msrc_dss_tcc_mclks = reg(0x46).get() + 1; + timeouts->msrc_dss_tcc_us = + timeout_mclks_to_microseconds_(timeouts->msrc_dss_tcc_mclks, timeouts->pre_range_vcsel_period_pclks); + + uint16_t value; + read_byte_16(0x51, &value); + timeouts->pre_range_mclks = decode_timeout_(value); + timeouts->pre_range_us = + timeout_mclks_to_microseconds_(timeouts->pre_range_mclks, timeouts->pre_range_vcsel_period_pclks); + + timeouts->final_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_FINAL_RANGE); + + read_byte_16(0x71, &value); + timeouts->final_range_mclks = decode_timeout_(value); + + if (enables->pre_range) { + timeouts->final_range_mclks -= timeouts->pre_range_mclks; + } + + timeouts->final_range_us = + timeout_mclks_to_microseconds_(timeouts->final_range_mclks, timeouts->final_range_vcsel_period_pclks); + } + + uint8_t get_vcsel_pulse_period_(VcselPeriodType type) { + uint8_t vcsel; + if (type == VCSEL_PERIOD_PRE_RANGE) + vcsel = reg(0x50).get(); + else if (type == VCSEL_PERIOD_FINAL_RANGE) + vcsel = reg(0x70).get(); + else + return 255; + + return (vcsel + 1) << 1; + } + + uint32_t get_macro_period_(uint8_t vcsel_period_pclks) { + return ((2304UL * vcsel_period_pclks * 1655UL) + 500UL) / 1000UL; + } + + uint32_t timeout_mclks_to_microseconds_(uint16_t timeout_period_mclks, uint8_t vcsel_period_pclks) { + uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks); + return ((timeout_period_mclks * macro_period_ns) + (macro_period_ns / 2)) / 1000; + } + uint32_t timeout_microseconds_to_mclks_(uint32_t timeout_period_us, uint8_t vcsel_period_pclks) { + uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks); + return (((timeout_period_us * 1000) + (macro_period_ns / 2)) / macro_period_ns); + } + + uint16_t decode_timeout_(uint16_t reg_val) { + // format: "(LSByte * 2^MSByte) + 1" + uint8_t msb = (reg_val >> 8) & 0xFF; + uint8_t lsb = (reg_val >> 0) & 0xFF; + return (uint16_t(lsb) << msb) + 1; + } + uint16_t encode_timeout_(uint16_t timeout_mclks) { + // format: "(LSByte * 2^MSByte) + 1" + uint32_t ls_byte = 0; + uint16_t ms_byte = 0; + + if (timeout_mclks <= 0) + return 0; + + ls_byte = timeout_mclks - 1; + + while ((ls_byte & 0xFFFFFF00) > 0) { + ls_byte >>= 1; + ms_byte++; + } + + return (ms_byte << 8) | (ls_byte & 0xFF); + } + + bool perform_single_ref_calibration_(uint8_t vhv_init_byte) { + reg(0x00) = 0x01 | vhv_init_byte; // VL53L0X_REG_SYSRANGE_MODE_START_STOP + + uint32_t start = millis(); + while ((reg(0x13).get() & 0x07) == 0) { + if (millis() - start > 1000) + return false; + yield(); + } + + reg(0x0B) = 0x01; + reg(0x00) = 0x00; + + return true; + } + + float signal_rate_limit_; + uint32_t measurement_timing_budget_us_; + bool initiated_read_{false}; + bool waiting_for_interrupt_{false}; + uint8_t stop_variable_; +}; + +} // namespace vl53l0x +} // namespace esphome diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index cb7de80918..a8ffbcc538 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -21,6 +21,7 @@ WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum('WaveshareEPaperTypeBModel' MODELS = { '1.54in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN), '2.13in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN), + '2.13in-ttgo': ('a', WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN), '2.90in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), '2.70in': ('b', WaveshareEPaper2P7In), '4.20in': ('b', WaveshareEPaper4P2In), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 2fe12dc102..c2f7acde40 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -8,13 +8,45 @@ namespace waveshare_epaper { static const char *TAG = "waveshare_epaper"; -static const uint8_t FULL_UPDATE_LUT[30] = {0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69, - 0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00, - 0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00}; +static const uint8_t LUT_SIZE_WAVESHARE = 30; +static const uint8_t FULL_UPDATE_LUT[LUT_SIZE_WAVESHARE] = {0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69, + 0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00}; -static const uint8_t PARTIAL_UPDATE_LUT[30] = {0x10, 0x18, 0x18, 0x08, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t PARTIAL_UPDATE_LUT[LUT_SIZE_WAVESHARE] = { + 0x10, 0x18, 0x18, 0x08, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const uint8_t LUT_SIZE_TTGO = 70; +static const uint8_t FULL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { + 0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, // LUT0: BB: VS 0 ~7 + 0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00, // LUT1: BW: VS 0 ~7 + 0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, // LUT2: WB: VS 0 ~7 + 0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00, // LUT3: WW: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT4: VCOM: VS 0 ~7 + 0x03, 0x03, 0x00, 0x00, 0x02, // TP0 A~D RP0 + 0x09, 0x09, 0x00, 0x00, 0x02, // TP1 A~D RP1 + 0x03, 0x03, 0x00, 0x00, 0x02, // TP2 A~D RP2 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP3 A~D RP3 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP4 A~D RP4 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP5 A~D RP5 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP6 A~D RP6 +}; + +static const uint8_t PARTIAL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT0: BB: VS 0 ~7 + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT1: BW: VS 0 ~7 + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT2: WB: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT3: WW: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT4: VCOM: VS 0 ~7 + 0x0A, 0x00, 0x00, 0x00, 0x00, // TP0 A~D RP0 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP1 A~D RP1 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP2 A~D RP2 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP3 A~D RP3 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP4 A~D RP4 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP5 A~D RP5 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP6 A~D RP6 +}; void WaveshareEPaper::setup_pins_() { this->init_internal_(this->get_buffer_length_()); @@ -42,7 +74,6 @@ void WaveshareEPaper::data(uint8_t value) { this->write_byte(value); this->end_data_(); } -bool WaveshareEPaper::is_device_msb_first() { return true; } bool WaveshareEPaper::wait_until_idle_() { if (this->busy_pin_ == nullptr) { return true; @@ -81,7 +112,6 @@ void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, int color) this->buffer_[pos] &= ~(0x80 >> subpos); } uint32_t WaveshareEPaper::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal() / 8u; } -bool WaveshareEPaper::is_device_high_speed() { return true; } void WaveshareEPaper::start_command_() { this->dc_pin_->digital_write(false); this->enable(); @@ -136,6 +166,9 @@ void WaveshareEPaperTypeA::dump_config() { case WAVESHARE_EPAPER_2_13_IN: ESP_LOGCONFIG(TAG, " Model: 2.13in"); break; + case TTGO_EPAPER_2_13_IN: + ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO)"); + break; case WAVESHARE_EPAPER_2_9_IN: ESP_LOGCONFIG(TAG, " Model: 2.9in"); break; @@ -156,7 +189,11 @@ void HOT WaveshareEPaperTypeA::display() { bool prev_full_update = this->at_update_ == 1; bool full_update = this->at_update_ == 0; if (full_update != prev_full_update) { - this->write_lut_(full_update ? FULL_UPDATE_LUT : PARTIAL_UPDATE_LUT); + if (this->model_ == TTGO_EPAPER_2_13_IN) { + this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO : PARTIAL_UPDATE_LUT_TTGO, LUT_SIZE_TTGO); + } else { + this->write_lut_(full_update ? FULL_UPDATE_LUT : PARTIAL_UPDATE_LUT, LUT_SIZE_WAVESHARE); + } } this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; } @@ -208,6 +245,8 @@ int WaveshareEPaperTypeA::get_width_internal() { return 200; case WAVESHARE_EPAPER_2_13_IN: return 128; + case TTGO_EPAPER_2_13_IN: + return 128; case WAVESHARE_EPAPER_2_9_IN: return 128; } @@ -219,15 +258,17 @@ int WaveshareEPaperTypeA::get_height_internal() { return 200; case WAVESHARE_EPAPER_2_13_IN: return 250; + case TTGO_EPAPER_2_13_IN: + return 250; case WAVESHARE_EPAPER_2_9_IN: return 296; } return 0; } -void WaveshareEPaperTypeA::write_lut_(const uint8_t *lut) { +void WaveshareEPaperTypeA::write_lut_(const uint8_t *lut, const uint8_t size) { // COMMAND WRITE LUT REGISTER this->command(0x32); - for (uint8_t i = 0; i < 30; i++) + for (uint8_t i = 0; i < size; i++) this->data(lut[i]); } WaveshareEPaperTypeA::WaveshareEPaperTypeA(WaveshareEPaperTypeAModel model) : model_(model) {} @@ -495,7 +536,6 @@ void HOT WaveshareEPaper4P2In::display() { } int WaveshareEPaper4P2In::get_width_internal() { return 400; } int WaveshareEPaper4P2In::get_height_internal() { return 300; } -bool WaveshareEPaper4P2In::is_device_high_speed() { return false; } void WaveshareEPaper4P2In::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); ESP_LOGCONFIG(TAG, " Model: 4.2in"); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 192e85275e..eff6b895a9 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -7,14 +7,16 @@ namespace esphome { namespace waveshare_epaper { -class WaveshareEPaper : public PollingComponent, public spi::SPIDevice, public display::DisplayBuffer { +class WaveshareEPaper : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, + spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } - bool is_device_msb_first() override; void command(uint8_t value); void data(uint8_t value); @@ -43,16 +45,14 @@ class WaveshareEPaper : public PollingComponent, public spi::SPIDevice, public d void reset_() { if (this->reset_pin_ != nullptr) { this->reset_pin_->digital_write(false); - delay(200); + delay(200); // NOLINT this->reset_pin_->digital_write(true); - delay(200); + delay(200); // NOLINT } } uint32_t get_buffer_length_(); - bool is_device_high_speed() override; - void start_command_(); void end_command_(); void start_data_(); @@ -67,6 +67,7 @@ enum WaveshareEPaperTypeAModel { WAVESHARE_EPAPER_1_54_IN = 0, WAVESHARE_EPAPER_2_13_IN, WAVESHARE_EPAPER_2_9_IN, + TTGO_EPAPER_2_13_IN, }; class WaveshareEPaperTypeA : public WaveshareEPaper { @@ -88,7 +89,7 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { void set_full_update_every(uint32_t full_update_every); protected: - void write_lut_(const uint8_t *lut); + void write_lut_(const uint8_t *lut, uint8_t size); int get_width_internal() override; @@ -143,7 +144,7 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { // COMMAND PANEL SETTING this->command(0x00); - delay(100); + delay(100); // NOLINT // COMMAND POWER SETTING this->command(0x01); @@ -152,7 +153,7 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { this->data(0x00); this->data(0x00); this->data(0x00); - delay(100); + delay(100); // NOLINT // COMMAND POWER OFF this->command(0x02); @@ -166,8 +167,6 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { int get_width_internal() override; int get_height_internal() override; - - bool is_device_high_speed() override; }; class WaveshareEPaper7P5In : public WaveshareEPaper { diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 206fc2c733..04f3cc5c04 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -1,10 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_CSS_URL, CONF_ID, CONF_JS_URL, CONF_PORT -from esphome.core import CORE, coroutine_with_priority +from esphome.components import web_server_base +from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID +from esphome.const import ( + CONF_CSS_URL, CONF_ID, CONF_JS_URL, CONF_PORT, + CONF_AUTH, CONF_USERNAME, CONF_PASSWORD) +from esphome.core import coroutine_with_priority -DEPENDENCIES = ['network'] -AUTO_LOAD = ['json'] +AUTO_LOAD = ['json', 'web_server_base'] web_server_ns = cg.esphome_ns.namespace('web_server') WebServer = web_server_ns.class_('WebServer', cg.Component, cg.Controller) @@ -14,18 +17,25 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_PORT, default=80): cv.port, cv.Optional(CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css"): cv.string, cv.Optional(CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js"): cv.string, + cv.Optional(CONF_AUTH): cv.Schema({ + cv.Required(CONF_USERNAME): cv.string_strict, + cv.Required(CONF_PASSWORD): cv.string_strict, + }), + + cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(web_server_base.WebServerBase), }).extend(cv.COMPONENT_SCHEMA) @coroutine_with_priority(40.0) def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) + + var = cg.new_Pvariable(config[CONF_ID], paren) yield cg.register_component(var, config) - cg.add(var.set_port(config[CONF_PORT])) + cg.add(paren.set_port(config[CONF_PORT])) cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) - - if CORE.is_esp32: - cg.add_library('FS', None) - cg.add_library('ESP Async WebServer', '1.1.1') + if CONF_AUTH in config: + cg.add(var.set_username(config[CONF_AUTH][CONF_USERNAME])) + cg.add(var.set_password(config[CONF_AUTH][CONF_PASSWORD])) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 882af4b995..4fdbbbce7d 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -6,15 +6,11 @@ #include "StreamString.h" -#ifdef ARDUINO_ARCH_ESP32 -#include <Update.h> -#endif -#ifdef ARDUINO_ARCH_ESP8266 -#include <Updater.h> -#endif - #include <cstdlib> + +#ifdef USE_LOGGER #include <esphome/components/logger/logger.h> +#endif namespace esphome { namespace web_server { @@ -66,7 +62,7 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::setup() { ESP_LOGCONFIG(TAG, "Setting up web server..."); - this->server_ = new AsyncWebServer(this->port_); + this->base_->init(); this->events_.onConnect([this](AsyncEventSourceClient *client) { // Configure reconnect timeout @@ -114,91 +110,21 @@ void WebServer::setup() { logger::global_logger->add_on_log_callback( [this](int level, const char *tag, const char *message) { this->events_.send(message, "log", millis()); }); #endif - this->server_->addHandler(this); - this->server_->addHandler(&this->events_); - - this->server_->begin(); + this->base_->add_handler(&this->events_); + this->base_->add_handler(this); + this->base_->add_ota_handler(); this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); }); } void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:"); - ESP_LOGCONFIG(TAG, " Address: %s:%u", network_get_address().c_str(), this->port_); + ESP_LOGCONFIG(TAG, " Address: %s:%u", network_get_address().c_str(), this->base_->get_port()); + if (this->using_auth()) { + ESP_LOGCONFIG(TAG, " Basic authentication enabled"); + } } float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; } -void WebServer::handle_update_request(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response; - if (!Update.hasError()) { - response = request->beginResponse(200, "text/plain", "Update Successful!"); - } else { - StreamString ss; - ss.print("Update Failed: "); - Update.printError(ss); - response = request->beginResponse(200, "text/plain", ss); - } - response->addHeader("Connection", "close"); - request->send(response); -} - -void report_ota_error() { - StreamString ss; - Update.printError(ss); - ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str()); -} - -void WebServer::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, - size_t len, bool final) { - bool success; - if (index == 0) { - ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str()); - this->ota_read_length_ = 0; -#ifdef ARDUINO_ARCH_ESP8266 - Update.runAsync(true); - success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); -#endif -#ifdef ARDUINO_ARCH_ESP32 - if (Update.isRunning()) - Update.abort(); - success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH); -#endif - if (!success) { - report_ota_error(); - return; - } - } else if (Update.hasError()) { - // don't spam logs with errors if something failed at start - return; - } - - success = Update.write(data, len) == len; - if (!success) { - report_ota_error(); - return; - } - this->ota_read_length_ += len; - - const uint32_t now = millis(); - if (now - this->last_ota_progress_ > 1000) { - if (request->contentLength() != 0) { - float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); - ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); - } else { - ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); - } - this->last_ota_progress_ = now; - } - - if (final) { - if (Update.end(true)) { - ESP_LOGI(TAG, "OTA update successful!"); - this->set_timeout(100, []() { App.safe_reboot(); }); - } else { - report_ota_error(); - } - } -} - void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); std::string title = App.get_name() + " Web Server"; @@ -248,7 +174,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for " "REST API documentation.</p>" - "<h2>OTA Update</h2><form method='POST' action=\"/update\" enctype=\"multipart/form-data\"><input " + "<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input " "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>" "<h2>Debug Log</h2><pre id=\"log\"></pre>" "<script src=\"")); @@ -490,11 +416,15 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, UrlMatch ma if (request->hasParam("color_temp")) call.set_color_temperature(request->getParam("color_temp")->value().toFloat()); - if (request->hasParam("flash")) - call.set_flash_length((uint32_t) request->getParam("flash")->value().toFloat() * 1000); + if (request->hasParam("flash")) { + float length_s = request->getParam("flash")->value().toFloat(); + call.set_flash_length(static_cast<uint32_t>(length_s * 1000)); + } - if (request->hasParam("transition")) - call.set_transition_length((uint32_t) request->getParam("transition")->value().toFloat() * 1000); + if (request->hasParam("transition")) { + float length_s = request->getParam("transition")->value().toFloat(); + call.set_transition_length(static_cast<uint32_t>(length_s * 1000)); + } if (request->hasParam("effect")) { const char *effect = request->getParam("effect")->value().c_str(); @@ -531,9 +461,6 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; - if (request->url() == "/update" && request->method() == HTTP_POST) - return true; - UrlMatch match = match_url(request->url().c_str(), true); if (!match.valid) return false; @@ -570,13 +497,12 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { - if (request->url() == "/") { - this->handle_index_request(request); - return; + if (this->using_auth() && !request->authenticate(this->username_, this->password_)) { + return request->requestAuthentication(); } - if (request->url() == "/update") { - this->handle_update_request(request); + if (request->url() == "/") { + this->handle_index_request(request); return; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 7840a81dce..4dca8200cc 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -2,9 +2,9 @@ #include "esphome/core/component.h" #include "esphome/core/controller.h" +#include "esphome/components/web_server_base/web_server_base.h" #include <vector> -#include <ESPAsyncWebServer.h> namespace esphome { namespace web_server { @@ -28,7 +28,11 @@ struct UrlMatch { */ class WebServer : public Controller, public Component, public AsyncWebHandler { public: - void set_port(uint16_t port) { port_ = port; } + WebServer(web_server_base::WebServerBase *base) : base_(base) {} + + void set_username(const char *username) { username_ = username; } + + void set_password(const char *password) { password_ = password; } /** Set the URL to the CSS <link> that's sent to each client. Defaults to * https://esphome.io/_static/webserver-v1.min.css @@ -57,7 +61,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle an index request under '/'. void handle_index_request(AsyncWebServerRequest *request); - void handle_update_request(AsyncWebServerRequest *request); + bool using_auth() { return username_ != nullptr && password_ != nullptr; } #ifdef USE_SENSOR void on_sensor_update(sensor::Sensor *obj, float state) override; @@ -122,19 +126,16 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. void handleRequest(AsyncWebServerRequest *request) override; - void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, - bool final) override; /// This web handle is not trivial. bool isRequestHandlerTrivial() override; protected: - uint16_t port_; - AsyncWebServer *server_; + web_server_base::WebServerBase *base_; AsyncEventSource events_{"/events"}; + const char *username_{nullptr}; + const char *password_{nullptr}; const char *css_url_{nullptr}; const char *js_url_{nullptr}; - uint32_t last_ota_progress_{0}; - uint32_t ota_read_length_{0}; }; } // namespace web_server diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py new file mode 100644 index 0000000000..923a594eb8 --- /dev/null +++ b/esphome/components/web_server_base/__init__.py @@ -0,0 +1,26 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from esphome.core import coroutine_with_priority, CORE + +DEPENDENCIES = ['network'] +AUTO_LOAD = ['async_tcp'] + +web_server_base_ns = cg.esphome_ns.namespace('web_server_base') +WebServerBase = web_server_base_ns.class_('WebServerBase', cg.Component) + +CONF_WEB_SERVER_BASE_ID = 'web_server_base_id' +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(WebServerBase), +}) + + +@coroutine_with_priority(65.0) +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + if CORE.is_esp32: + cg.add_library('FS', None) + # https://github.com/OttoWinter/ESPAsyncWebServer/blob/master/library.json + cg.add_library('ESPAsyncWebServer-esphome', '1.2.5') diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp new file mode 100644 index 0000000000..b7548504e3 --- /dev/null +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -0,0 +1,96 @@ +#include "web_server_base.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include <StreamString.h> + +#ifdef ARDUINO_ARCH_ESP32 +#include <Update.h> +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#include <Updater.h> +#endif + +namespace esphome { +namespace web_server_base { + +static const char *TAG = "web_server_base"; + +void report_ota_error() { + StreamString ss; + Update.printError(ss); + ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str()); +} + +void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, + uint8_t *data, size_t len, bool final) { + bool success; + if (index == 0) { + ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str()); + this->ota_read_length_ = 0; +#ifdef ARDUINO_ARCH_ESP8266 + Update.runAsync(true); + success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); +#endif +#ifdef ARDUINO_ARCH_ESP32 + if (Update.isRunning()) + Update.abort(); + success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH); +#endif + if (!success) { + report_ota_error(); + return; + } + } else if (Update.hasError()) { + // don't spam logs with errors if something failed at start + return; + } + + success = Update.write(data, len) == len; + if (!success) { + report_ota_error(); + return; + } + this->ota_read_length_ += len; + + const uint32_t now = millis(); + if (now - this->last_ota_progress_ > 1000) { + if (request->contentLength() != 0) { + float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); + ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); + } else { + ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); + } + this->last_ota_progress_ = now; + } + + if (final) { + if (Update.end(true)) { + ESP_LOGI(TAG, "OTA update successful!"); + this->parent_->set_timeout(100, []() { App.safe_reboot(); }); + } else { + report_ota_error(); + } + } +} +void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response; + if (!Update.hasError()) { + response = request->beginResponse(200, "text/plain", "Update Successful!"); + } else { + StreamString ss; + ss.print("Update Failed: "); + Update.printError(ss); + response = request->beginResponse(200, "text/plain", ss); + } + response->addHeader("Connection", "close"); + request->send(response); +} + +void WebServerBase::add_ota_handler() { this->add_handler(new OTARequestHandler(this)); } +float WebServerBase::get_setup_priority() const { + // Before WiFi (captive portal) + return setup_priority::WIFI + 2.0f; +} + +} // namespace web_server_base +} // namespace esphome diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h new file mode 100644 index 0000000000..b6024ceafa --- /dev/null +++ b/esphome/components/web_server_base/web_server_base.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/core/component.h" + +#include <ESPAsyncWebServer.h> + +namespace esphome { +namespace web_server_base { + +class WebServerBase : public Component { + public: + void init() { + if (this->initialized_) { + this->initialized_++; + return; + } + this->server_ = new AsyncWebServer(this->port_); + this->server_->begin(); + + for (auto *handler : this->handlers_) + this->server_->addHandler(handler); + + this->initialized_++; + } + void deinit() { + this->initialized_--; + if (this->initialized_ == 0) { + delete this->server_; + this->server_ = nullptr; + } + } + AsyncWebServer *get_server() const { return server_; } + float get_setup_priority() const override; + + void add_handler(AsyncWebHandler *handler) { + // remove all handlers + + this->handlers_.push_back(handler); + if (this->server_ != nullptr) + this->server_->addHandler(handler); + } + + void add_ota_handler(); + + void set_port(uint16_t port) { port_ = port; } + uint16_t get_port() const { return port_; } + + protected: + friend class OTARequestHandler; + + int initialized_{0}; + uint16_t port_{80}; + AsyncWebServer *server_{nullptr}; + std::vector<AsyncWebHandler *> handlers_; +}; + +class OTARequestHandler : public AsyncWebHandler { + public: + OTARequestHandler(WebServerBase *parent) : parent_(parent) {} + void handleRequest(AsyncWebServerRequest *request) override; + void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, + bool final) override; + bool canHandle(AsyncWebServerRequest *request) override { + return request->url() == "/update" && request->method() == HTTP_POST; + } + + bool isRequestHandlerTrivial() override { return false; } + + protected: + uint32_t last_ota_progress_{0}; + uint32_t ota_read_length_{0}; + WebServerBase *parent_; +}; + +} // namespace web_server_base +} // namespace esphome diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 9ac8b74ef2..93f2d59564 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -5,7 +5,7 @@ from esphome.automation import Condition from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \ CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \ - CONF_SUBNET, CONF_USE_ADDRESS + CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY from esphome.core import CORE, HexInt, coroutine_with_priority AUTO_LOAD = ['network'] @@ -64,13 +64,15 @@ WIFI_NETWORK_BASE = cv.Schema({ cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, }) +CONF_AP_TIMEOUT = 'ap_timeout' WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({ - + cv.Optional(CONF_AP_TIMEOUT, default='1min'): cv.positive_time_period_milliseconds, }) WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ cv.Optional(CONF_BSSID): cv.mac_address, cv.Optional(CONF_HIDDEN): cv.boolean, + cv.Optional(CONF_PRIORITY, default=0.0): cv.float_, }) @@ -118,8 +120,9 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_AP): WIFI_NETWORK_AP, cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, - cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_POWER_SAVE_MODE, default='NONE'): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), + cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds, + cv.SplitDefault(CONF_POWER_SAVE_MODE, esp8266='none', esp32='light'): + cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, @@ -160,25 +163,28 @@ def wifi_network(config, static_ip): cg.add(ap.set_channel(config[CONF_CHANNEL])) if static_ip is not None: cg.add(ap.set_manual_ip(manual_ip(static_ip))) + if CONF_PRIORITY in config: + cg.add(ap.set_priority(config[CONF_PRIORITY])) return ap @coroutine_with_priority(60.0) def to_code(config): - rhs = WiFiComponent.new() - wifi = cg.Pvariable(config[CONF_ID], rhs) - cg.add(wifi.set_use_address(config[CONF_USE_ADDRESS])) + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) for network in config.get(CONF_NETWORKS, []): - cg.add(wifi.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP)))) + cg.add(var.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP)))) if CONF_AP in config: - cg.add(wifi.set_ap(wifi_network(config[CONF_AP], config.get(CONF_MANUAL_IP)))) + conf = config[CONF_AP] + cg.add(var.set_ap(wifi_network(conf, config.get(CONF_MANUAL_IP)))) + cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) - cg.add(wifi.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) - cg.add(wifi.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) - cg.add(wifi.set_fast_connect(config[CONF_FAST_CONNECT])) + cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) + cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) + cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT])) if CORE.is_esp8266: cg.add_library('ESP8266WiFi', None) @@ -186,7 +192,7 @@ def to_code(config): cg.add_define('USE_WIFI') # Register at end for OTA safe mode - yield cg.register_component(wifi, config) + yield cg.register_component(var, config) @automation.register_condition('wifi.connected', WiFiConnectedCondition, cv.Schema({})) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 28ff54826f..cb664d3cc3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -18,6 +18,10 @@ #include "esphome/core/util.h" #include "esphome/core/application.h" +#ifdef USE_CAPTIVE_PORTAL +#include "esphome/components/captive_portal/captive_portal.h" +#endif + namespace esphome { namespace wifi { @@ -27,20 +31,15 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up WiFi..."); - - this->wifi_register_callbacks_(); - - bool ret = this->wifi_mode_(this->has_sta(), false); - if (!ret) { - this->mark_failed(); - return; - } + this->last_connected_ = millis(); + this->wifi_pre_setup_(); if (this->has_sta()) { - this->wifi_disable_auto_connect_(); - delay(10); + this->wifi_sta_pre_setup_(); - this->wifi_apply_power_save_(); + if (!this->wifi_apply_power_save_()) { + ESP_LOGV(TAG, "Setting Power Save Option failed!"); + } if (this->fast_connect_) { this->selected_ap_ = this->sta_[0]; @@ -50,10 +49,16 @@ void WiFiComponent::setup() { } } else if (this->has_ap()) { this->setup_ap_config_(); +#ifdef USE_CAPTIVE_PORTAL + if (captive_portal::global_captive_portal != nullptr) + captive_portal::global_captive_portal->start(); +#endif } this->wifi_apply_hostname_(); +#ifdef ARDUINO_ARCH_ESP32 network_setup_mdns(); +#endif } void WiFiComponent::loop() { @@ -99,6 +104,17 @@ void WiFiComponent::loop() { break; } + if (this->has_ap() && !this->ap_setup_) { + if (now - this->last_connected_ > this->ap_timeout_) { + ESP_LOGI(TAG, "Starting fallback AP!"); + this->setup_ap_config_(); +#ifdef USE_CAPTIVE_PORTAL + if (captive_portal::global_captive_portal != nullptr) + captive_portal::global_captive_portal->start(); +#endif + } + } + if (!this->has_ap() && this->reboot_timeout_ != 0) { if (now - this->last_connected_ > this->reboot_timeout_) { ESP_LOGE(TAG, "Can't connect to WiFi, rebooting..."); @@ -119,7 +135,7 @@ IPAddress WiFiComponent::get_ip_address() { if (this->has_sta()) return this->wifi_sta_ip_(); if (this->has_ap()) - return this->wifi_soft_ap_ip_(); + return this->wifi_soft_ap_ip(); return {}; } std::string WiFiComponent::get_use_address() const { @@ -147,7 +163,10 @@ void WiFiComponent::setup_ap_config_() { } this->ap_setup_ = this->wifi_start_ap_(this->ap_); - ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip_().toString().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().toString().c_str()); +#ifdef ARDUINO_ARCH_ESP8266 + network_setup_mdns(this->wifi_soft_ap_ip(), 1); +#endif if (!this->has_sta()) { this->state_ = WIFI_COMPONENT_STATE_AP; @@ -159,6 +178,10 @@ float WiFiComponent::get_loop_priority() const { } void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } +void WiFiComponent::set_sta(const WiFiAP &ap) { + this->sta_.clear(); + this->add_sta(ap); +} void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { ESP_LOGI(TAG, "WiFi Connecting to '%s'...", ap.get_ssid().c_str()); @@ -260,6 +283,9 @@ void WiFiComponent::print_connect_params_() { int8_t rssi = WiFi.RSSI(); print_signal_bars(rssi, signal_bars); ESP_LOGCONFIG(TAG, " Signal strength: %d dB %s", rssi, signal_bars); + if (this->selected_ap_.get_bssid().has_value()) { + ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); + } ESP_LOGCONFIG(TAG, " Channel: %d", WiFi.channel()); ESP_LOGCONFIG(TAG, " Subnet: %s", WiFi.subnetMask().toString().c_str()); ESP_LOGCONFIG(TAG, " Gateway: %s", WiFi.gatewayIP().toString().c_str()); @@ -295,6 +321,10 @@ void WiFiComponent::check_scanning_finished() { for (auto &ap : this->sta_) { if (res.matches(ap)) { res.set_matches(true); + if (!this->has_sta_priority(res.get_bssid())) { + this->set_sta_priority(res.get_bssid(), ap.get_priority()); + } + res.set_priority(this->get_sta_priority(res.get_bssid())); break; } } @@ -302,11 +332,18 @@ void WiFiComponent::check_scanning_finished() { std::stable_sort(this->scan_result_.begin(), this->scan_result_.end(), [](const WiFiScanResult &a, const WiFiScanResult &b) { + // return true if a is better than b if (a.get_matches() && !b.get_matches()) return true; if (!a.get_matches() && b.get_matches()) return false; + if (a.get_matches() && b.get_matches()) { + // if both match, check priority + if (a.get_priority() != b.get_priority()) + return a.get_priority() > b.get_priority(); + } + return a.get_rssi() > b.get_rssi(); }); @@ -377,13 +414,21 @@ void WiFiComponent::check_connecting_finished() { wl_status_t status = this->wifi_sta_status_(); if (status == WL_CONNECTED) { - ESP_LOGI(TAG, "WiFi connected!"); + ESP_LOGI(TAG, "WiFi Connected!"); this->print_connect_params_(); if (this->has_ap()) { +#ifdef USE_CAPTIVE_PORTAL + if (this->is_captive_portal_active_()) { + captive_portal::global_captive_portal->end(); + } +#endif ESP_LOGD(TAG, "Disabling AP..."); this->wifi_mode_({}, false); } +#ifdef ARDUINO_ARCH_ESP8266 + network_setup_mdns(this->wifi_sta_ip_(), 0); +#endif this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; return; @@ -425,12 +470,18 @@ void WiFiComponent::check_connecting_finished() { } void WiFiComponent::retry_connect() { + if (this->selected_ap_.get_bssid()) { + auto bssid = *this->selected_ap_.get_bssid(); + float priority = this->get_sta_priority(bssid); + this->set_sta_priority(bssid, priority - 1.0f); + } + delay(10); - if (this->num_retried_ > 5 || this->error_from_callback_) { + if (!this->is_captive_portal_active_() && (this->num_retried_ > 5 || this->error_from_callback_)) { // If retry failed for more than 5 times, let's restart STA ESP_LOGW(TAG, "Restarting WiFi adapter..."); this->wifi_mode_(false, {}); - delay(100); + delay(100); // NOLINT this->num_retried_ = 0; } else { this->num_retried_++; @@ -443,9 +494,6 @@ void WiFiComponent::retry_connect() { return; } - if (this->has_ap()) { - this->setup_ap_config_(); - } this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; this->action_started_ = millis(); } @@ -461,11 +509,6 @@ bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_status_() == WL_CONNECTED && !this->error_from_callback_; } -bool WiFiComponent::ready_for_ota() { - if (this->has_ap()) - return true; - return this->is_connected(); -} void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; } std::string WiFiComponent::format_mac_addr(const uint8_t *mac) { @@ -473,20 +516,12 @@ std::string WiFiComponent::format_mac_addr(const uint8_t *mac) { sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); return buf; } - -bool sta_field_equal(const uint8_t *field_a, const uint8_t *field_b, int len) { - for (int i = 0; i < len; i++) { - uint8_t a = field_a[i]; - uint8_t b = field_b[i]; - if (a == b && a == 0) - break; - if (a == b) - continue; - - return false; - } - - return true; +bool WiFiComponent::is_captive_portal_active_() { +#ifdef USE_CAPTIVE_PORTAL + return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active(); +#else + return false; +#endif } void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 8e6418791c..04866ef8e2 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -66,12 +66,14 @@ class WiFiAP { void set_bssid(optional<bssid_t> bssid); void set_password(const std::string &password); void set_channel(optional<uint8_t> channel); + void set_priority(float priority) { priority_ = priority; } void set_manual_ip(optional<ManualIP> manual_ip); void set_hidden(bool hidden); const std::string &get_ssid() const; const optional<bssid_t> &get_bssid() const; const std::string &get_password() const; const optional<uint8_t> &get_channel() const; + float get_priority() const { return priority_; } const optional<ManualIP> &get_manual_ip() const; bool get_hidden() const; @@ -80,6 +82,7 @@ class WiFiAP { optional<bssid_t> bssid_; std::string password_; optional<uint8_t> channel_; + float priority_{0}; optional<ManualIP> manual_ip_; bool hidden_{false}; }; @@ -99,6 +102,8 @@ class WiFiScanResult { int8_t get_rssi() const; bool get_with_auth() const; bool get_is_hidden() const; + float get_priority() const { return priority_; } + void set_priority(float priority) { priority_ = priority; } protected: bool matches_{false}; @@ -108,6 +113,12 @@ class WiFiScanResult { int8_t rssi_; bool with_auth_; bool is_hidden_; + float priority_{0.0f}; +}; + +struct WiFiSTAPriority { + bssid_t bssid; + float priority; }; enum WiFiPowerSaveMode { @@ -122,6 +133,7 @@ class WiFiComponent : public Component { /// Construct a WiFiComponent. WiFiComponent(); + void set_sta(const WiFiAP &ap); void add_sta(const WiFiAP &ap); /** Setup an Access Point that should be created if no connection to a station can be made. @@ -137,6 +149,7 @@ class WiFiComponent : public Component { void check_scanning_finished(); void start_connecting(const WiFiAP &ap, bool two); void set_fast_connect(bool fast_connect); + void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; } void check_connecting_finished(); @@ -144,8 +157,6 @@ class WiFiComponent : public Component { bool can_proceed() override; - bool ready_for_ota(); - void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected(); @@ -171,24 +182,54 @@ class WiFiComponent : public Component { std::string get_use_address() const; void set_use_address(const std::string &use_address); + const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; } + + IPAddress wifi_soft_ap_ip(); + + bool has_sta_priority(const bssid_t &bssid) { + for (auto &it : this->sta_priorities_) + if (it.bssid == bssid) + return true; + return false; + } + float get_sta_priority(const bssid_t bssid) { + for (auto &it : this->sta_priorities_) + if (it.bssid == bssid) + return it.priority; + return 0.0f; + } + void set_sta_priority(const bssid_t bssid, float priority) { + for (auto &it : this->sta_priorities_) + if (it.bssid == bssid) { + it.priority = priority; + return; + } + this->sta_priorities_.push_back(WiFiSTAPriority{ + .bssid = bssid, + .priority = priority, + }); + } + protected: static std::string format_mac_addr(const uint8_t mac[6]); void setup_ap_config_(); void print_connect_params_(); bool wifi_mode_(optional<bool> sta, optional<bool> ap); - bool wifi_disable_auto_connect_(); + bool wifi_sta_pre_setup_(); bool wifi_apply_power_save_(); bool wifi_sta_ip_config_(optional<ManualIP> manual_ip); IPAddress wifi_sta_ip_(); bool wifi_apply_hostname_(); bool wifi_sta_connect_(WiFiAP ap); - void wifi_register_callbacks_(); + void wifi_pre_setup_(); wl_status_t wifi_sta_status_(); bool wifi_scan_start_(); bool wifi_ap_ip_config_(optional<ManualIP> manual_ip); bool wifi_start_ap_(const WiFiAP &ap); - IPAddress wifi_soft_ap_ip_(); + bool wifi_disconnect_(); + + bool is_captive_portal_active_(); #ifdef ARDUINO_ARCH_ESP8266 static void wifi_event_callback(System_Event_t *event); @@ -203,6 +244,7 @@ class WiFiComponent : public Component { std::string use_address_; std::vector<WiFiAP> sta_; + std::vector<WiFiSTAPriority> sta_priorities_; WiFiAP selected_ap_; bool fast_connect_{false}; @@ -211,7 +253,8 @@ class WiFiComponent : public Component { uint32_t action_started_; uint8_t num_retried_{0}; uint32_t last_connected_{0}; - uint32_t reboot_timeout_{300000}; + uint32_t reboot_timeout_{}; + uint32_t ap_timeout_{}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; bool error_from_callback_{false}; std::vector<WiFiScanResult> scan_result_; diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index 6b118cf0ae..353a51040a 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -53,8 +53,12 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) { return ret; } -bool WiFiComponent::wifi_disable_auto_connect_() { +bool WiFiComponent::wifi_sta_pre_setup_() { + if (!this->wifi_mode_(true, {})) + return false; + WiFi.setAutoReconnect(false); + delay(10); return true; } bool WiFiComponent::wifi_apply_power_save_() { @@ -158,10 +162,16 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { conf.sta.channel = *ap.get_channel(); } - esp_err_t err = esp_wifi_disconnect(); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_disconnect failed! %d", err); - return false; + wifi_config_t current_conf; + esp_err_t err; + esp_wifi_get_config(WIFI_IF_STA, ¤t_conf); + + if (memcmp(¤t_conf, &conf, sizeof(wifi_config_t)) != 0) { + err = esp_wifi_disconnect(); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_disconnect failed! %d", err); + return false; + } } err = esp_wifi_set_config(WIFI_IF_STA, &conf); @@ -359,7 +369,7 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i } case SYSTEM_EVENT_AP_PROBEREQRECVED: { auto it = info.ap_probereqrecved; - ESP_LOGV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } default: @@ -382,10 +392,12 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i this->wifi_scan_done_callback_(); } } -void WiFiComponent::wifi_register_callbacks_() { +void WiFiComponent::wifi_pre_setup_() { auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); WiFi.onEvent(f); WiFi.persistent(false); + // Make sure WiFi is in clean state before anything starts + this->wifi_mode_(false, false); } wl_status_t WiFiComponent::wifi_sta_status_() { return WiFi.status(); } bool WiFiComponent::wifi_scan_start_() { @@ -517,11 +529,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } -IPAddress WiFiComponent::wifi_soft_ap_ip_() { +IPAddress WiFiComponent::wifi_soft_ap_ip() { tcpip_adapter_ip_info_t ip; tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); return IPAddress(ip.ip.addr); } +bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } } // namespace wifi } // namespace esphome diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index afd5b7c0cc..ed6e616b3d 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -58,19 +58,6 @@ bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) { return ret; } -bool WiFiComponent::wifi_disable_auto_connect_() { - bool ret1, ret2; - ETS_UART_INTR_DISABLE(); - ret1 = wifi_station_set_auto_connect(0); - ret2 = wifi_station_set_reconnect_policy(false); - ETS_UART_INTR_ENABLE(); - - if (!ret1 || !ret2) { - ESP_LOGV(TAG, "Disabling Auto-Connect failed!"); - } - - return ret1 && ret2; -} bool WiFiComponent::wifi_apply_power_save_() { sleep_type_t power_save; switch (this->power_save_) { @@ -158,9 +145,7 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { if (!this->wifi_mode_(true, {})) return false; - ETS_UART_INTR_DISABLE(); - wifi_station_disconnect(); - ETS_UART_INTR_ENABLE(); + this->wifi_disconnect_(); struct station_config conf {}; memset(&conf, 0, sizeof(conf)); @@ -330,11 +315,6 @@ const char *get_disconnect_reason_str(uint8_t reason) { } void WiFiComponent::wifi_event_callback(System_Event_t *event) { - // TODO: this callback is called while in cont context, so delay will fail - // We need to defer the log messages until we're out of this context - // only affects verbose log level - // reproducible by enabling verbose log level and letting the ESP disconnect and - // then reconnect to WiFi. switch (event->event) { case EVENT_STAMODE_CONNECTED: { auto it = event->event_info.connected; @@ -382,7 +362,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_SOFTAPMODE_PROBEREQRECVED: { auto it = event->event_info.ap_probereqrecved; - ESP_LOGV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); break; } #ifndef ARDUINO_ESP8266_RELEASE_2_3_0 @@ -410,7 +390,62 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { WiFiMockClass::_event_callback(event); } -void WiFiComponent::wifi_register_callbacks_() { wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback); } +bool WiFiComponent::wifi_sta_pre_setup_() { + if (!this->wifi_mode_(true, {})) + return false; + + // Clear saved STA config + station_config default_config{}; + wifi_station_get_config_default(&default_config); + bool is_zero = default_config.ssid[0] == '\0' && default_config.password[0] == '\0' && default_config.bssid[0] == 0 && + default_config.bssid_set == 0; + if (!is_zero) { + ESP_LOGV(TAG, "Clearing default wifi STA config"); + + memset(&default_config, 0, sizeof(default_config)); + ETS_UART_INTR_DISABLE(); + bool ret = wifi_station_set_config(&default_config); + ETS_UART_INTR_ENABLE(); + + if (!ret) { + ESP_LOGW(TAG, "Clearing default wif STA config failed!"); + } + } + + bool ret1, ret2; + ETS_UART_INTR_DISABLE(); + ret1 = wifi_station_set_auto_connect(0); + ret2 = wifi_station_set_reconnect_policy(false); + ETS_UART_INTR_ENABLE(); + + if (!ret1 || !ret2) { + ESP_LOGV(TAG, "Disabling Auto-Connect failed!"); + } + + delay(10); + return true; +} + +void WiFiComponent::wifi_pre_setup_() { + wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback); + // Make sure the default opmode is OFF + uint8_t default_opmode = wifi_get_opmode_default(); + if (default_opmode != 0) { + ESP_LOGV(TAG, "Setting default WiFi Mode to 0 (was %u)", default_opmode); + + ETS_UART_INTR_DISABLE(); + bool ret = wifi_set_opmode(0); + ETS_UART_INTR_ENABLE(); + + if (!ret) { + ESP_LOGW(TAG, "Setting default WiFi mode failed!"); + } + } + + // Make sure WiFi is in clean state before anything starts + this->wifi_mode_(false, false); +} + wl_status_t WiFiComponent::wifi_sta_status_() { station_status_t status = wifi_station_get_connect_status(); switch (status) { @@ -435,11 +470,6 @@ bool WiFiComponent::wifi_scan_start_() { if (!this->wifi_mode_(true, {})) return false; - station_status_t sta_status = wifi_station_get_connect_status(); - if (sta_status != STATION_GOT_IP && sta_status != STATION_IDLE) { - wifi_station_disconnect(); - } - struct scan_config config {}; memset(&config, 0, sizeof(config)); config.ssid = nullptr; @@ -465,6 +495,15 @@ bool WiFiComponent::wifi_scan_start_() { return ret; } +bool WiFiComponent::wifi_disconnect_() { + station_config conf{}; + memset(&conf, 0, sizeof(conf)); + ETS_UART_INTR_DISABLE(); + wifi_station_set_config(&conf); + bool ret = wifi_station_disconnect(); + ETS_UART_INTR_ENABLE(); + return ret; +} void WiFiComponent::s_wifi_scan_done_callback(void *arg, STATUS status) { global_wifi_component->wifi_scan_done_callback_(arg, status); } @@ -583,7 +622,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } -IPAddress WiFiComponent::wifi_soft_ap_ip_() { +IPAddress WiFiComponent::wifi_soft_ap_ip() { struct ip_info ip {}; wifi_get_ip_info(SOFTAP_IF, &ip); return {ip.ip.addr}; diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp new file mode 100644 index 0000000000..704d9b3099 --- /dev/null +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -0,0 +1,14 @@ +#include "wifi_info_text_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace wifi_info { + +static const char *TAG = "wifi_info"; + +void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); } +void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); } +void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); } + +} // namespace wifi_info +} // namespace esphome diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 13e632bde1..9dfa684b4b 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -17,6 +17,8 @@ class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + std::string unique_id() override { return get_mac_address() + "-wifiinfo-ip"; } + void dump_config() override; protected: IPAddress last_ip_; @@ -32,6 +34,8 @@ class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + std::string unique_id() override { return get_mac_address() + "-wifiinfo-ssid"; } + void dump_config() override; protected: std::string last_ssid_; @@ -49,6 +53,8 @@ class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + std::string unique_id() override { return get_mac_address() + "-wifiinfo-bssid"; } + void dump_config() override; protected: wifi::bssid_t last_bssid_; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 4367b5fd1e..1172f6ee0a 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -81,15 +81,17 @@ optional<XiaomiParseResult> parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d return {}; } - bool is_mijia = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; - bool is_miflora = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; + bool is_lywsdcgq = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; + bool is_hhccjcy01 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; + bool is_lywsd02 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x5b && raw[3] == 0x04; + bool is_cgg1 = (raw[1] & 0x30) == 0x30 && raw[2] == 0x47 && raw[3] == 0x03; - if (!is_mijia && !is_miflora) { + if (!is_lywsdcgq && !is_hhccjcy01 && !is_lywsd02 && !is_cgg1) { // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); return {}; } - uint8_t raw_offset = is_mijia ? 11 : 12; + uint8_t raw_offset = is_lywsdcgq || is_cgg1 ? 11 : 12; const uint8_t raw_type = raw[raw_offset]; const uint8_t data_length = raw[raw_offset + 2]; @@ -101,7 +103,14 @@ optional<XiaomiParseResult> parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d return {}; } XiaomiParseResult result; - result.type = is_miflora ? XiaomiParseResult::TYPE_MIFLORA : XiaomiParseResult::TYPE_MIJIA; + result.type = XiaomiParseResult::TYPE_HHCCJCY01; + if (is_lywsdcgq) { + result.type = XiaomiParseResult::TYPE_LYWSDCGQ; + } else if (is_lywsd02) { + result.type = XiaomiParseResult::TYPE_LYWSD02; + } else if (is_cgg1) { + result.type = XiaomiParseResult::TYPE_CGG1; + } bool success = parse_xiaomi_data_byte(raw_type, data, data_length, result); if (!success) return {}; @@ -113,7 +122,14 @@ bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if (!res.has_value()) return false; - const char *name = res->type == XiaomiParseResult::TYPE_MIFLORA ? "Mi Flora" : "Mi Jia"; + const char *name = "HHCCJCY01"; + if (res->type == XiaomiParseResult::TYPE_LYWSDCGQ) { + name = "LYWSDCGQ"; + } else if (res->type == XiaomiParseResult::TYPE_LYWSD02) { + name = "LYWSD02"; + } else if (res->type == XiaomiParseResult::TYPE_CGG1) { + name = "CGG1"; + } ESP_LOGD(TAG, "Got Xiaomi %s (%s):", name, device.address_str().c_str()); diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index 058a89927b..824ea80edf 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -9,7 +9,7 @@ namespace esphome { namespace xiaomi_ble { struct XiaomiParseResult { - enum { TYPE_MIJIA, TYPE_MIFLORA } type; + enum { TYPE_LYWSDCGQ, TYPE_HHCCJCY01, TYPE_LYWSD02, TYPE_CGG1 } type; optional<float> temperature; optional<float> humidity; optional<float> battery_level; diff --git a/esphome/components/xiaomi_cgg1/__init__.py b/esphome/components/xiaomi_cgg1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_cgg1/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py new file mode 100644 index 0000000000..897687c68a --- /dev/null +++ b/esphome/components/xiaomi_cgg1/sensor.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_cgg1_ns = cg.esphome_ns.namespace('xiaomi_cgg1') +XiaomiCGG1 = xiaomi_cgg1_ns.class_( + 'XiaomiCGG1', esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiCGG1), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp similarity index 59% rename from esphome/components/xiaomi_mijia/xiaomi_mijia.cpp rename to esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index 544af32d7b..6cc14f5a8e 100644 --- a/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -1,21 +1,21 @@ -#include "xiaomi_mijia.h" +#include "xiaomi_cgg1.h" #include "esphome/core/log.h" #ifdef ARDUINO_ARCH_ESP32 namespace esphome { -namespace xiaomi_mijia { +namespace xiaomi_cgg1 { -static const char *TAG = "xiaomi_mijia"; +static const char *TAG = "xiaomi_cgg1"; -void XiaomiMijia::dump_config() { - ESP_LOGCONFIG(TAG, "Xiaomi Mijia"); +void XiaomiCGG1::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi CGG1"); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); } -} // namespace xiaomi_mijia +} // namespace xiaomi_cgg1 } // namespace esphome #endif diff --git a/esphome/components/xiaomi_mijia/xiaomi_mijia.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h similarity index 91% rename from esphome/components/xiaomi_mijia/xiaomi_mijia.h rename to esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index 814e33fa75..7f73011275 100644 --- a/esphome/components/xiaomi_mijia/xiaomi_mijia.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -8,9 +8,9 @@ #ifdef ARDUINO_ARCH_ESP32 namespace esphome { -namespace xiaomi_mijia { +namespace xiaomi_cgg1 { -class XiaomiMijia : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } @@ -44,7 +44,7 @@ class XiaomiMijia : public Component, public esp32_ble_tracker::ESPBTDeviceListe sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_mijia +} // namespace xiaomi_cgg1 } // namespace esphome #endif diff --git a/esphome/components/xiaomi_hhccjcy01/__init__.py b/esphome/components/xiaomi_hhccjcy01/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py new file mode 100644 index 0000000000..495446ba11 --- /dev/null +++ b/esphome/components/xiaomi_hhccjcy01/sensor.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ + UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_hhccjcy01_ns = cg.esphome_ns.namespace('xiaomi_hhccjcy01') +XiaomiHHCCJCY01 = xiaomi_hhccjcy01_ns.class_('XiaomiHHCCJCY01', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiHHCCJCY01), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), + cv.Optional(CONF_CONDUCTIVITY): + sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_MOISTURE in config: + sens = yield sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture(sens)) + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) + if CONF_CONDUCTIVITY in config: + sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) + cg.add(var.set_conductivity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp similarity index 64% rename from esphome/components/xiaomi_miflora/xiaomi_miflora.cpp rename to esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp index 966c78a1a6..8c8152c54c 100644 --- a/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp @@ -1,15 +1,15 @@ -#include "xiaomi_miflora.h" +#include "xiaomi_hhccjcy01.h" #include "esphome/core/log.h" #ifdef ARDUINO_ARCH_ESP32 namespace esphome { -namespace xiaomi_miflora { +namespace xiaomi_hhccjcy01 { -static const char *TAG = "xiaomi_miflora"; +static const char *TAG = "xiaomi_hhccjcy01"; -void XiaomiMiflora::dump_config() { - ESP_LOGCONFIG(TAG, "Xiaomi Mijia"); +void XiaomiHHCCJCY01::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi HHCCJCY01"); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Moisture", this->moisture_); LOG_SENSOR(" ", "Conductivity", this->conductivity_); @@ -17,7 +17,7 @@ void XiaomiMiflora::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_level_); } -} // namespace xiaomi_miflora +} // namespace xiaomi_hhccjcy01 } // namespace esphome #endif diff --git a/esphome/components/xiaomi_miflora/xiaomi_miflora.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h similarity index 93% rename from esphome/components/xiaomi_miflora/xiaomi_miflora.h rename to esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index d1f05cdcc7..c1b8511bb8 100644 --- a/esphome/components/xiaomi_miflora/xiaomi_miflora.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -8,9 +8,9 @@ #ifdef ARDUINO_ARCH_ESP32 namespace esphome { -namespace xiaomi_miflora { +namespace xiaomi_hhccjcy01 { -class XiaomiMiflora : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } @@ -52,7 +52,7 @@ class XiaomiMiflora : public Component, public esp32_ble_tracker::ESPBTDeviceLis sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_miflora +} // namespace xiaomi_hhccjcy01 } // namespace esphome #endif diff --git a/esphome/components/xiaomi_lywsd02/__init__.py b/esphome/components/xiaomi_lywsd02/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py new file mode 100644 index 0000000000..8e4d59316b --- /dev/null +++ b/esphome/components/xiaomi_lywsd02/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_lywsd02_ns = cg.esphome_ns.namespace('xiaomi_lywsd02') +XiaomiLYWSD02 = xiaomi_lywsd02_ns.class_('XiaomiLYWSD02', esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiLYWSD02), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp new file mode 100644 index 0000000000..cd77c133a5 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp @@ -0,0 +1,20 @@ +#include "xiaomi_lywsd02.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_lywsd02 { + +static const char *TAG = "xiaomi_lywsd02"; + +void XiaomiLYWSD02::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); +} + +} // namespace xiaomi_lywsd02 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h new file mode 100644 index 0000000000..9b8aba1bb0 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_lywsd02 { + +class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + if (device.address_uint64() != this->address_) + return false; + + auto res = xiaomi_ble::parse_xiaomi(device); + if (!res.has_value()) + return false; + + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + return true; + } + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; +}; + +} // namespace xiaomi_lywsd02 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsdcgq/__init__.py b/esphome/components/xiaomi_lywsdcgq/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_lywsdcgq/sensor.py b/esphome/components/xiaomi_lywsdcgq/sensor.py new file mode 100644 index 0000000000..e13c860464 --- /dev/null +++ b/esphome/components/xiaomi_lywsdcgq/sensor.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_lywsdcgq_ns = cg.esphome_ns.namespace('xiaomi_lywsdcgq') +XiaomiLYWSDCGQ = xiaomi_lywsdcgq_ns.class_('XiaomiLYWSDCGQ', esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiLYWSDCGQ), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp new file mode 100644 index 0000000000..2dacff2876 --- /dev/null +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp @@ -0,0 +1,21 @@ +#include "xiaomi_lywsdcgq.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_lywsdcgq { + +static const char *TAG = "xiaomi_lywsdcgq"; + +void XiaomiLYWSDCGQ::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi LYWSDCGQ"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +} // namespace xiaomi_lywsdcgq +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h new file mode 100644 index 0000000000..b6756eec61 --- /dev/null +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_lywsdcgq { + +class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + if (device.address_uint64() != this->address_) + return false; + + auto res = xiaomi_ble::parse_xiaomi(device); + if (!res.has_value()) + return false; + + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + return true; + } + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_lywsdcgq +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_miflora/sensor.py b/esphome/components/xiaomi_miflora/sensor.py index 8be06a93f3..0a0b3ff63f 100644 --- a/esphome/components/xiaomi_miflora/sensor.py +++ b/esphome/components/xiaomi_miflora/sensor.py @@ -1,49 +1,3 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ - CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ - UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER -DEPENDENCIES = ['esp32_ble_tracker'] -AUTO_LOAD = ['xiaomi_ble'] - -xiaomi_miflora_ns = cg.esphome_ns.namespace('xiaomi_miflora') -XiaomiMiflora = xiaomi_miflora_ns.class_('XiaomiMiflora', esp32_ble_tracker.ESPBTDeviceListener, - cg.Component) - -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(XiaomiMiflora), - cv.Required(CONF_MAC_ADDRESS): cv.mac_address, - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), - cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), - cv.Optional(CONF_CONDUCTIVITY): - sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), - cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) - - -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield esp32_ble_tracker.register_ble_device(var, config) - - cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) - - if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) - cg.add(var.set_temperature(sens)) - if CONF_MOISTURE in config: - sens = yield sensor.new_sensor(config[CONF_MOISTURE]) - cg.add(var.set_moisture(sens)) - if CONF_ILLUMINANCE in config: - sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) - cg.add(var.set_illuminance(sens)) - if CONF_CONDUCTIVITY in config: - sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) - cg.add(var.set_conductivity(sens)) - if CONF_BATTERY_LEVEL in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) - cg.add(var.set_battery_level(sens)) +CONFIG_SCHEMA = cv.invalid("This sensor has been renamed to xiaomi_hhccjcy01") diff --git a/esphome/components/xiaomi_mijia/sensor.py b/esphome/components/xiaomi_mijia/sensor.py index 995a6cbf25..597d8d1bce 100644 --- a/esphome/components/xiaomi_mijia/sensor.py +++ b/esphome/components/xiaomi_mijia/sensor.py @@ -1,38 +1,3 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID -DEPENDENCIES = ['esp32_ble_tracker'] -AUTO_LOAD = ['xiaomi_ble'] - -xiaomi_mijia_ns = cg.esphome_ns.namespace('xiaomi_mijia') -XiaomiMijia = xiaomi_mijia_ns.class_('XiaomiMijia', esp32_ble_tracker.ESPBTDeviceListener, - cg.Component) - -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(XiaomiMijia), - cv.Required(CONF_MAC_ADDRESS): cv.mac_address, - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), - cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) - - -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield esp32_ble_tracker.register_ble_device(var, config) - - cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) - - if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) - cg.add(var.set_temperature(sens)) - if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) - cg.add(var.set_humidity(sens)) - if CONF_BATTERY_LEVEL in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) - cg.add(var.set_battery_level(sens)) +CONFIG_SCHEMA = cv.invalid("This sensor has been renamed to xiaomi_lywsdcgq") diff --git a/esphome/components/yashima/__init__.py b/esphome/components/yashima/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/yashima/climate.py b/esphome/components/yashima/climate.py new file mode 100644 index 0000000000..4c4b98d9e7 --- /dev/null +++ b/esphome/components/yashima/climate.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, remote_transmitter, sensor +from esphome.components.remote_base import CONF_TRANSMITTER_ID +from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT + +AUTO_LOAD = ['sensor'] + +yashima_ns = cg.esphome_ns.namespace('yashima') +YashimaClimate = yashima_ns.class_('YashimaClimate', climate.Climate, cg.Component) + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(YashimaClimate), + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), + cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), +}).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) + cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) + if CONF_SENSOR in config: + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + + transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp new file mode 100644 index 0000000000..e3c0a33127 --- /dev/null +++ b/esphome/components/yashima/yashima.cpp @@ -0,0 +1,193 @@ +#include "yashima.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace yashima { + +static const char *TAG = "yashima.climate"; + +const uint16_t YASHIMA_STATE_LENGTH = 9; +const uint16_t YASHIMA_BITS = YASHIMA_STATE_LENGTH * 8; + +/* the bit masks are intended to be sent from the MSB to the LSB */ +const uint8_t YASHIMA_MODE_HEAT_BYTE0 = 0b00100000; +const uint8_t YASHIMA_MODE_DRY_BYTE0 = 0b01100000; +const uint8_t YASHIMA_MODE_COOL_BYTE0 = 0b11100000; +const uint8_t YASHIMA_MODE_FAN_BYTE0 = 0b10100000; +const uint8_t YASHIMA_MODE_AUTO_BYTE0 = 0b11100000; +const uint8_t YASHIMA_MODE_OFF_BYTE0 = 0b11110000; +const uint8_t YASHIMA_BASE_BYTE0 = 0b1110; + +const uint8_t YASHIMA_TEMP_MAX = 30; // Celsius +const uint8_t YASHIMA_TEMP_MIN = 16; // Celsius +const uint8_t YASHIMA_TEMP_RANGE = YASHIMA_TEMP_MAX - YASHIMA_TEMP_MIN + 1; + +const uint8_t YASHIMA_TEMP_MAP_BYTE1[YASHIMA_TEMP_RANGE] = { + 0b01100100, // 16C + 0b10100100, // 17C + 0b00100100, // 18C + 0b11000100, // 19C + 0b01000100, // 20C + 0b10000100, // 21C + 0b00000100, // 22C + 0b11111000, // 23C + 0b01111000, // 24C + 0b10111000, // 25C + 0b00111000, // 26C + 0b11011000, // 27C + 0b01011000, // 28C + 0b10011000, // 29C + 0b00011000, // 30C +}; +const uint8_t YASHIMA_BASE_BYTE1 = 0b11; + +const uint8_t YASHIMA_FAN_AUTO_BYTE2 = 0b11000000; +const uint8_t YASHIMA_FAN_LOW_BYTE2 = 0b00000000; +const uint8_t YASHIMA_FAN_MEDIUM_BYTE2 = 0b10000000; +const uint8_t YASHIMA_FAN_HIGH_BYTE2 = 0b01000000; +const uint8_t YASHIMA_BASE_BYTE2 = 0b111111; + +const uint8_t YASHIMA_BASE_BYTE3 = 0b11111111; +const uint8_t YASHIMA_BASE_BYTE4 = 0b11; + +const uint8_t YASHIMA_MODE_HEAT_BYTE5 = 0b00000000; +const uint8_t YASHIMA_MODE_DRY_BYTE5 = 0b00000000; +const uint8_t YASHIMA_MODE_FAN_BYTE5 = 0b00000000; +const uint8_t YASHIMA_MODE_AUTO_BYTE5 = 0b00000000; +const uint8_t YASHIMA_MODE_COOL_BYTE5 = 0b10000000; +const uint8_t YASHIMA_MODE_OFF_BYTE5 = 0b10000000; +const uint8_t YASHIMA_BASE_BYTE5 = 0b11111; + +const uint8_t YASHIMA_BASE_BYTE6 = 0b11111111; +const uint8_t YASHIMA_BASE_BYTE7 = 0b11111111; +const uint8_t YASHIMA_BASE_BYTE8 = 0b11001111; + +/* values sampled using a Broadlink Mini 3: */ +// const uint16_t YASHIMA_HEADER_MARK = 9600; +// const uint16_t YASHIMA_HEADER_SPACE = 4800; +// const uint16_t YASHIMA_BIT_MARK = 720; +// const uint16_t YASHIMA_ONE_SPACE = 550; +// const uint16_t YASHIMA_ZERO_SPACE = 1640; + +/* scaled values to get correct timing on ESP8266/ESP32: */ +const uint16_t YASHIMA_HEADER_MARK = 9035; +const uint16_t YASHIMA_HEADER_SPACE = 4517; +const uint16_t YASHIMA_BIT_MARK = 667; +const uint16_t YASHIMA_ONE_SPACE = 517; +const uint16_t YASHIMA_ZERO_SPACE = 1543; +const uint32_t YASHIMA_GAP = YASHIMA_HEADER_SPACE; + +const uint32_t YASHIMA_CARRIER_FREQUENCY = 38000; + +climate::ClimateTraits YashimaClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->sensor_ != nullptr); + traits.set_supports_auto_mode(true); + traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_two_point_target_temperature(false); + traits.set_supports_away(false); + traits.set_visual_min_temperature(YASHIMA_TEMP_MIN); + traits.set_visual_max_temperature(YASHIMA_TEMP_MAX); + traits.set_visual_temperature_step(1); + return traits; +} + +void YashimaClimate::setup() { + if (this->sensor_) { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + // current temperature changed, publish state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + } else + this->current_temperature = NAN; + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } else { + // restore from defaults + this->mode = climate::CLIMATE_MODE_OFF; + this->target_temperature = 24; + } +} + +void YashimaClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + + this->transmit_state_(); + this->publish_state(); +} + +void YashimaClimate::transmit_state_() { + uint8_t remote_state[YASHIMA_STATE_LENGTH] = {0}; + + remote_state[0] = YASHIMA_BASE_BYTE0; + remote_state[1] = YASHIMA_BASE_BYTE1; + remote_state[2] = YASHIMA_BASE_BYTE2; + remote_state[3] = YASHIMA_BASE_BYTE3; + remote_state[4] = YASHIMA_BASE_BYTE4; + remote_state[5] = YASHIMA_BASE_BYTE5; + remote_state[6] = YASHIMA_BASE_BYTE6; + remote_state[7] = YASHIMA_BASE_BYTE7; + remote_state[8] = YASHIMA_BASE_BYTE8; + + // Set mode + switch (this->mode) { + case climate::CLIMATE_MODE_AUTO: + remote_state[0] |= YASHIMA_MODE_AUTO_BYTE0; + remote_state[5] |= YASHIMA_MODE_AUTO_BYTE5; + break; + case climate::CLIMATE_MODE_COOL: + remote_state[0] |= YASHIMA_MODE_COOL_BYTE0; + remote_state[5] |= YASHIMA_MODE_COOL_BYTE5; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[0] |= YASHIMA_MODE_HEAT_BYTE0; + remote_state[5] |= YASHIMA_MODE_HEAT_BYTE5; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state[0] |= YASHIMA_MODE_OFF_BYTE0; + remote_state[5] |= YASHIMA_MODE_OFF_BYTE5; + break; + // TODO: CLIMATE_MODE_FAN_ONLY, CLIMATE_MODE_DRY are missing in esphome + } + + // TODO: missing support for fan speed + remote_state[2] |= YASHIMA_FAN_AUTO_BYTE2; + + // Set temperature + uint8_t safecelsius = std::max((uint8_t) this->target_temperature, YASHIMA_TEMP_MIN); + safecelsius = std::min(safecelsius, YASHIMA_TEMP_MAX); + remote_state[1] |= YASHIMA_TEMP_MAP_BYTE1[safecelsius - YASHIMA_TEMP_MIN]; + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(YASHIMA_CARRIER_FREQUENCY); + + // Header + data->mark(YASHIMA_HEADER_MARK); + data->space(YASHIMA_HEADER_SPACE); + // Data (sent from the MSB to the LSB) + for (uint8_t i : remote_state) + for (int8_t j = 7; j >= 0; j--) { + data->mark(YASHIMA_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? YASHIMA_ONE_SPACE : YASHIMA_ZERO_SPACE); + } + // Footer + data->mark(YASHIMA_BIT_MARK); + data->space(YASHIMA_GAP); + + transmit.perform(); +} + +} // namespace yashima +} // namespace esphome diff --git a/esphome/components/yashima/yashima.h b/esphome/components/yashima/yashima.h new file mode 100644 index 0000000000..466816bd5f --- /dev/null +++ b/esphome/components/yashima/yashima.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/remote_base/remote_base.h" +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace yashima { + +class YashimaClimate : public climate::Climate, public Component { + public: + void setup() override; + void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { + this->transmitter_ = transmitter; + } + void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } + void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } + void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + + 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; + + /// Transmit via IR the state of this climate controller. + void transmit_state_(); + + bool supports_cool_{true}; + bool supports_heat_{true}; + + remote_transmitter::RemoteTransmitterComponent *transmitter_; + sensor::Sensor *sensor_{nullptr}; +}; + +} // namespace yashima +} // namespace esphome diff --git a/esphome/components/zyaura/__init__.py b/esphome/components/zyaura/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py new file mode 100644 index 0000000000..649b80b444 --- /dev/null +++ b/esphome/components/zyaura/sensor.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_CLOCK_PIN, CONF_DATA_PIN, \ + CONF_CO2, CONF_TEMPERATURE, CONF_HUMIDITY, \ + UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, UNIT_PERCENT, \ + ICON_PERIODIC_TABLE_CO2, ICON_THERMOMETER, ICON_WATER_PERCENT +from esphome.cpp_helpers import gpio_pin_expression + +zyaura_ns = cg.esphome_ns.namespace('zyaura') +ZyAuraSensor = zyaura_ns.class_('ZyAuraSensor', cg.PollingComponent) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(ZyAuraSensor), + cv.Required(CONF_CLOCK_PIN): cv.All(pins.internal_gpio_input_pin_schema, + pins.validate_has_interrupt), + cv.Required(CONF_DATA_PIN): cv.All(pins.internal_gpio_input_pin_schema, + pins.validate_has_interrupt), + cv.Optional(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), +}).extend(cv.polling_component_schema('60s')) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + pin_clock = yield gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_pin_clock(pin_clock)) + pin_data = yield gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_pin_data(pin_data)) + + if CONF_CO2 in config: + sens = yield sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/zyaura/zyaura.cpp b/esphome/components/zyaura/zyaura.cpp new file mode 100644 index 0000000000..3b1a2a5069 --- /dev/null +++ b/esphome/components/zyaura/zyaura.cpp @@ -0,0 +1,117 @@ +#include "zyaura.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace zyaura { + +static const char *TAG = "zyaura"; + +bool ICACHE_RAM_ATTR ZaDataProcessor::decode(unsigned long ms, bool data) { + // check if a new message has started, based on time since previous bit + if ((ms - this->prev_ms_) > ZA_MAX_MS) { + this->num_bits_ = 0; + } + this->prev_ms_ = ms; + + // number of bits received is basically the "state" + if (this->num_bits_ < ZA_FRAME_SIZE) { + // store it while it fits + int idx = this->num_bits_ / 8; + this->buffer_[idx] = (this->buffer_[idx] << 1) | (data ? 1 : 0); + this->num_bits_++; + + // are we done yet? + if (this->num_bits_ == ZA_FRAME_SIZE) { + // validate checksum + uint8_t checksum = this->buffer_[ZA_BYTE_TYPE] + this->buffer_[ZA_BYTE_HIGH] + this->buffer_[ZA_BYTE_LOW]; + if (checksum != this->buffer_[ZA_BYTE_SUM] || this->buffer_[ZA_BYTE_END] != ZA_MSG_DELIMETER) { + return false; + } + + this->message->type = (ZaDataType) this->buffer_[ZA_BYTE_TYPE]; + this->message->value = this->buffer_[ZA_BYTE_HIGH] << 8 | this->buffer_[ZA_BYTE_LOW]; + return true; + } + } + + return false; +} + +void ZaSensorStore::setup(GPIOPin *pin_clock, GPIOPin *pin_data) { + pin_clock->setup(); + pin_data->setup(); + this->pin_clock_ = pin_clock->to_isr(); + this->pin_data_ = pin_data->to_isr(); + pin_clock->attach_interrupt(ZaSensorStore::interrupt, this, FALLING); +} + +void ICACHE_RAM_ATTR ZaSensorStore::interrupt(ZaSensorStore *arg) { + uint32_t now = millis(); + bool data_bit = arg->pin_data_->digital_read(); + + if (arg->processor_.decode(now, data_bit)) { + arg->set_data_(arg->processor_.message); + } +} + +void ICACHE_RAM_ATTR ZaSensorStore::set_data_(ZaMessage *message) { + switch (message->type) { + case HUMIDITY: + this->humidity = (message->value > 10000) ? NAN : (message->value / 100.0f); + break; + + case TEMPERATURE: + this->temperature = (message->value > 5970) ? NAN : (message->value / 16.0f - 273.15f); + break; + + case CO2: + this->co2 = (message->value > 10000) ? NAN : message->value; + break; + + default: + break; + } +} + +bool ZyAuraSensor::publish_state_(sensor::Sensor *sensor, float *value) { + // Sensor doesn't added to configuration + if (sensor == nullptr) { + return true; + } + + sensor->publish_state(*value); + + // Sensor reported wrong value + if (isnan(*value)) { + ESP_LOGW(TAG, "Sensor reported invalid data. Is the update interval too small?"); + this->status_set_warning(); + return false; + } + + *value = NAN; + return true; +} + +void ZyAuraSensor::dump_config() { + ESP_LOGCONFIG(TAG, "ZyAuraSensor:"); + LOG_PIN(" Pin Clock: ", this->pin_clock_); + LOG_PIN(" Pin Data: ", this->pin_data_); + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void ZyAuraSensor::update() { + bool co2_result = this->publish_state_(this->co2_sensor_, &this->store_.co2); + bool temperature_result = this->publish_state_(this->temperature_sensor_, &this->store_.temperature); + bool humidity_result = this->publish_state_(this->humidity_sensor_, &this->store_.humidity); + + if (co2_result && temperature_result && humidity_result) { + this->status_clear_warning(); + } +} + +} // namespace zyaura +} // namespace esphome diff --git a/esphome/components/zyaura/zyaura.h b/esphome/components/zyaura/zyaura.h new file mode 100644 index 0000000000..fd26947e28 --- /dev/null +++ b/esphome/components/zyaura/zyaura.h @@ -0,0 +1,86 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace zyaura { + +static const uint8_t ZA_MAX_MS = 2; +static const uint8_t ZA_MSG_LEN = 5; +static const uint8_t ZA_FRAME_SIZE = 40; +static const uint8_t ZA_MSG_DELIMETER = 0x0D; + +static const uint8_t ZA_BYTE_TYPE = 0; +static const uint8_t ZA_BYTE_HIGH = 1; +static const uint8_t ZA_BYTE_LOW = 2; +static const uint8_t ZA_BYTE_SUM = 3; +static const uint8_t ZA_BYTE_END = 4; + +enum ZaDataType { + HUMIDITY = 0x41, + TEMPERATURE = 0x42, + CO2 = 0x50, +}; + +struct ZaMessage { + ZaDataType type; + uint16_t value; +}; + +class ZaDataProcessor { + public: + bool decode(unsigned long ms, bool data); + ZaMessage *message = new ZaMessage; + + protected: + uint8_t buffer_[ZA_MSG_LEN]; + int num_bits_ = 0; + unsigned long prev_ms_; +}; + +class ZaSensorStore { + public: + float co2 = NAN; + float temperature = NAN; + float humidity = NAN; + + void setup(GPIOPin *pin_clock, GPIOPin *pin_data); + static void interrupt(ZaSensorStore *arg); + + protected: + ISRInternalGPIOPin *pin_clock_; + ISRInternalGPIOPin *pin_data_; + ZaDataProcessor processor_; + + void set_data_(ZaMessage *message); +}; + +/// Component for reading temperature/co2/humidity measurements from ZyAura sensors. +class ZyAuraSensor : public PollingComponent { + public: + void set_pin_clock(GPIOPin *pin) { pin_clock_ = pin; } + void set_pin_data(GPIOPin *pin) { pin_data_ = pin; } + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + + void setup() override { this->store_.setup(this->pin_clock_, this->pin_data_); } + void dump_config() override; + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + ZaSensorStore store_; + GPIOPin *pin_clock_; + GPIOPin *pin_data_; + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + + bool publish_state_(sensor::Sensor *sensor, float *value); +}; + +} // namespace zyaura +} // namespace esphome diff --git a/esphome/config.py b/esphome/config.py index b5075e5e84..5906e2fc95 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -18,7 +18,7 @@ from esphome.components.substitutions import CONF_SUBSTITUTIONS from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent -from esphome.py_compat import text_type, IS_PY2 +from esphome.py_compat import text_type, IS_PY2, decode_text from esphome.util import safe_print, OrderedDict from typing import List, Optional, Tuple, Union # noqa @@ -634,8 +634,13 @@ def _format_vol_invalid(ex, config): class InvalidYAMLError(EsphomeError): def __init__(self, base_exc): + try: + base = str(base_exc) + except UnicodeDecodeError: + base = repr(base_exc) + base = decode_text(base) message = u"Invalid YAML syntax. Please see YAML syntax reference or use an " \ - u"online YAML syntax validator:\n\n{}".format(base_exc) + u"online YAML syntax validator:\n\n{}".format(base) super(InvalidYAMLError, self).__init__(message) self.base_exc = base_exc @@ -797,15 +802,15 @@ def strip_default_ids(config): return config -def read_config(verbose): - _LOGGER.info("Reading configuration...") +def read_config(): + _LOGGER.info("Reading configuration %s...", CORE.config_path) try: res = load_config() except EsphomeError as err: _LOGGER.error(u"Error while reading config: %s", err) return None if res.errors: - if not verbose: + if not CORE.verbose: res = strip_default_ids(res) safe_print(color('bold_red', u"Failed config")) diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index ddad36f8a8..0c508d2202 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -1,10 +1,10 @@ from __future__ import print_function -import codecs import json import os -from esphome.core import CORE, EsphomeError +from esphome.core import CORE +from esphome.helpers import read_file from esphome.py_compat import safe_input @@ -20,10 +20,4 @@ def read_config_file(path): assert data['type'] == 'file_response' return data['content'] - try: - with codecs.open(path, encoding='utf-8') as handle: - return handle.read() - except IOError as exc: - raise EsphomeError(u"Error accessing file {}: {}".format(path, exc)) - except UnicodeDecodeError as exc: - raise EsphomeError(u"Unable to read file {}: {}".format(path, exc)) + return read_file(path) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 4ec3033452..956779f655 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -61,7 +61,7 @@ RESERVED_IDS = [ 'App', 'pinMode', 'delay', 'delayMicroseconds', 'digitalRead', 'digitalWrite', 'INPUT', 'OUTPUT', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', - 'close', 'pause', 'sleep', 'open', + 'close', 'pause', 'sleep', 'open', 'setup', 'loop', ] @@ -80,6 +80,7 @@ class Optional(vol.Optional): during config validation - specifically *not* in the C++ code or the code generation phase. """ + def __init__(self, key, default=UNDEFINED): super(Optional, self).__init__(key, default=default) @@ -91,6 +92,7 @@ class Required(vol.Required): All required values should be acceessed with the `config[CONF_<KEY>]` syntax in code - *not* the `config.get(CONF_<KEY>)` syntax. """ + def __init__(self, key): super(Required, self).__init__(key) @@ -459,6 +461,8 @@ def time_period_str_unit(value): if isinstance(value, int): raise Invalid("Don't know what '{0}' means as it has no time *unit*! Did you mean " "'{0}s'?".format(value)) + if isinstance(value, TimePeriod): + value = str(value) if not isinstance(value, string_types): raise Invalid("Expected string for time period with unit.") @@ -1021,8 +1025,24 @@ def dimensions(value): def directory(value): + import json + from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) + + if CORE.vscode and (not CORE.ace or + os.path.abspath(path) == os.path.abspath(CORE.config_path)): + print(json.dumps({ + 'type': 'check_directory_exists', + 'path': path, + })) + data = json.loads(safe_input()) + assert data['type'] == 'directory_exists_response' + if data['content']: + return value + raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." + u"".format(path, os.path.abspath(path))) + if not os.path.exists(path): raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." u"".format(path, os.path.abspath(path))) @@ -1033,8 +1053,24 @@ def directory(value): def file_(value): + import json + from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) + + if CORE.vscode and (not CORE.ace or + os.path.abspath(path) == os.path.abspath(CORE.config_path)): + print(json.dumps({ + 'type': 'check_file_exists', + 'path': path, + })) + data = json.loads(safe_input()) + assert data['type'] == 'file_exists_response' + if data['content']: + return value + raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." + u"".format(path, os.path.abspath(path))) + if not os.path.exists(path): raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." u"".format(path, os.path.abspath(path))) @@ -1100,12 +1136,14 @@ def typed_schema(schemas, **kwargs): class GenerateID(Optional): """Mark this key as being an auto-generated ID key.""" + def __init__(self, key=CONF_ID): super(GenerateID, self).__init__(key, default=lambda: None) class SplitDefault(Optional): """Mark this key to have a split default for ESP8266/ESP32.""" + def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED): super(SplitDefault, self).__init__(key) self._esp8266_default = vol.default_factory(esp8266) @@ -1127,6 +1165,7 @@ class SplitDefault(Optional): class OnlyWith(Optional): """Set the default value only if the given component is loaded.""" + def __init__(self, key, component, default=None): super(OnlyWith, self).__init__(key) self._component = component @@ -1191,6 +1230,8 @@ def validate_registry_entry(name, registry): registry_entry = registry[key] + value = value.copy() + with prepend_path([key]): value[key] = registry_entry.schema(value[key]) diff --git a/esphome/const.py b/esphome/const.py index 097e00672b..137785ae02 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -2,8 +2,8 @@ """Constants used by esphome.""" MAJOR_VERSION = 1 -MINOR_VERSION = 13 -PATCH_VERSION = '6' +MINOR_VERSION = 14 +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) @@ -15,11 +15,14 @@ ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage' ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0' ARDUINO_VERSION_ESP32_1_0_1 = 'espressif32@1.6.0' +ARDUINO_VERSION_ESP32_1_0_2 = 'espressif32@1.9.0' +ARDUINO_VERSION_ESP32_1_0_3 = 'espressif32@1.10.0' +ARDUINO_VERSION_ESP32_1_0_4 = 'espressif32@1.11.0' ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \ '/stage' ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.0' ARDUINO_VERSION_ESP8266_2_5_1 = 'espressif8266@2.1.0' -ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.0' +ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.3' ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0' SOURCE_FILE_EXTENSIONS = {'.cpp', '.hpp', '.h', '.c', '.tcc', '.ino'} HEADER_FILE_EXTENSIONS = {'.h', '.hpp', '.tcc'} @@ -38,6 +41,7 @@ CONF_ARGS = 'args' CONF_ASSUMED_STATE = 'assumed_state' CONF_AT = 'at' CONF_ATTENUATION = 'attenuation' +CONF_AUTH = 'auth' CONF_AUTOMATION_ID = 'automation_id' CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' @@ -59,10 +63,11 @@ CONF_BROKER = 'broker' CONF_BSSID = 'bssid' CONF_BUFFER_SIZE = 'buffer_size' CONF_BUILD_PATH = 'build_path' -CONF_BUSY_PIN = 'busy_pin' CONF_BUS_VOLTAGE = 'bus_voltage' +CONF_BUSY_PIN = 'busy_pin' CONF_CALIBRATE_LINEAR = 'calibrate_linear' CONF_CALIBRATION = 'calibration' +CONF_CAPACITANCE = 'capacitance' CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' CONF_CARRIER_FREQUENCY = 'carrier_frequency' CONF_CHANGE_MODE_EVERY = 'change_mode_every' @@ -79,22 +84,23 @@ CONF_CO2 = 'co2' CONF_CODE = 'code' CONF_COLD_WHITE = 'cold_white' CONF_COLD_WHITE_COLOR_TEMPERATURE = 'cold_white_color_temperature' -CONF_COLORS = 'colors' CONF_COLOR_CORRECT = 'color_correct' CONF_COLOR_TEMPERATURE = 'color_temperature' +CONF_COLORS = 'colors' CONF_COMMAND = 'command' CONF_COMMAND_TOPIC = 'command_topic' +CONF_COMMENT = 'comment' CONF_COMMIT = 'commit' -CONF_COMPONENTS = 'components' CONF_COMPONENT_ID = 'component_id' +CONF_COMPONENTS = 'components' CONF_CONDITION = 'condition' CONF_CONDITION_ID = 'condition_id' CONF_CONDUCTIVITY = 'conductivity' CONF_COOL_ACTION = 'cool_action' CONF_COUNT_MODE = 'count_mode' CONF_CRON = 'cron' -CONF_CSS_URL = 'css_url' CONF_CS_PIN = 'cs_pin' +CONF_CSS_URL = 'css_url' CONF_CURRENT = 'current' CONF_CURRENT_OPERATION = 'current_operation' CONF_CURRENT_RESISTOR = 'current_resistor' @@ -116,11 +122,13 @@ CONF_DELTA = 'delta' CONF_DEVICE = 'device' CONF_DEVICE_CLASS = 'device_class' CONF_DIMENSIONS = 'dimensions' -CONF_DIRECTION = 'direction' CONF_DIR_PIN = 'dir_pin' +CONF_DIRECTION = 'direction' CONF_DISCOVERY = 'discovery' CONF_DISCOVERY_PREFIX = 'discovery_prefix' CONF_DISCOVERY_RETAIN = 'discovery_retain' +CONF_DISTANCE = 'distance' +CONF_DIV_RATIO = 'div_ratio' CONF_DNS1 = 'dns1' CONF_DNS2 = 'dns2' CONF_DOMAIN = 'domain' @@ -136,6 +144,7 @@ CONF_ENTITY_ID = 'entity_id' CONF_ESP8266_RESTORE_FROM_FLASH = 'esp8266_restore_from_flash' CONF_ESPHOME = 'esphome' CONF_ESPHOME_CORE_VERSION = 'esphome_core_version' +CONF_EVENT = 'event' CONF_EXPIRE_AFTER = 'expire_after' CONF_EXTERNAL_VCC = 'external_vcc' CONF_FALLING_EDGE = 'falling_edge' @@ -143,10 +152,11 @@ CONF_FAMILY = 'family' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' -CONF_FILTERS = 'filters' CONF_FILTER_OUT = 'filter_out' +CONF_FILTERS = 'filters' CONF_FLASH_LENGTH = 'flash_length' CONF_FOR = 'for' +CONF_FORCE_UPDATE = 'force_update' CONF_FORMALDEHYDE = 'formaldehyde' CONF_FORMAT = 'format' CONF_FREQUENCY = 'frequency' @@ -162,8 +172,8 @@ CONF_GREEN = 'green' CONF_GROUP = 'group' CONF_HARDWARE_UART = 'hardware_uart' CONF_HEARTBEAT = 'heartbeat' -CONF_HEATER = 'heater' CONF_HEAT_ACTION = 'heat_action' +CONF_HEATER = 'heater' CONF_HIDDEN = 'hidden' CONF_HIGH = 'high' CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' @@ -182,6 +192,8 @@ CONF_IIR_FILTER = 'iir_filter' CONF_ILLUMINANCE = 'illuminance' CONF_INCLUDES = 'includes' CONF_INDEX = 'index' +CONF_INDOOR = 'indoor' +CONF_INITIAL_MODE = 'initial_mode' CONF_INITIAL_VALUE = 'initial_value' CONF_INTEGRATION_TIME = 'integration_time' CONF_INTENSITY = 'intensity' @@ -195,23 +207,26 @@ CONF_INVERTED = 'inverted' CONF_IP_ADDRESS = 'ip_address' CONF_JS_URL = 'js_url' CONF_JVC = 'jvc' -CONF_KEEPALIVE = 'keepalive' CONF_KEEP_ON_TIME = 'keep_on_time' +CONF_KEEPALIVE = 'keepalive' CONF_LAMBDA = 'lambda' CONF_LEVEL = 'level' CONF_LG = 'lg' CONF_LIBRARIES = 'libraries' CONF_LIGHT = 'light' +CONF_LIGHTNING_ENERGY = 'lightning_energy' +CONF_LIGHTNING_THRESHOLD = 'lightning_threshold' CONF_LOADED_INTEGRATIONS = 'loaded_integrations' CONF_LOCAL = 'local' +CONF_LOG_TOPIC = 'log_topic' CONF_LOGGER = 'logger' CONF_LOGS = 'logs' -CONF_LOG_TOPIC = 'log_topic' CONF_LOW = 'low' CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference' CONF_MAC_ADDRESS = 'mac_address' CONF_MAKE_ID = 'make_id' CONF_MANUAL_IP = 'manual_ip' +CONF_MASK_DISTURBER = 'mask_disturber' CONF_MAX_CURRENT = 'max_current' CONF_MAX_DURATION = 'max_duration' CONF_MAX_LENGTH = 'max_length' @@ -225,13 +240,13 @@ CONF_MAX_VOLTAGE = 'max_voltage' CONF_MEASUREMENT_DURATION = 'measurement_duration' CONF_MEDIUM = 'medium' CONF_METHOD = 'method' -CONF_MINUTE = 'minute' -CONF_MINUTES = 'minutes' CONF_MIN_LENGTH = 'min_length' CONF_MIN_LEVEL = 'min_level' CONF_MIN_POWER = 'min_power' CONF_MIN_TEMPERATURE = 'min_temperature' CONF_MIN_VALUE = 'min_value' +CONF_MINUTE = 'minute' +CONF_MINUTES = 'minutes' CONF_MISO_PIN = 'miso_pin' CONF_MODE = 'mode' CONF_MODEL = 'model' @@ -246,14 +261,14 @@ CONF_NAME = 'name' CONF_NBITS = 'nbits' CONF_NEC = 'nec' CONF_NETWORKS = 'networks' -CONF_NUMBER = 'number' +CONF_NOISE_LEVEL = 'noise_level' CONF_NUM_ATTEMPTS = 'num_attempts' CONF_NUM_CHANNELS = 'num_channels' CONF_NUM_CHIPS = 'num_chips' CONF_NUM_LEDS = 'num_leds' +CONF_NUMBER = 'number' CONF_OFFSET = 'offset' CONF_ON = 'on' -CONF_ONE = 'one' CONF_ON_BOOT = 'on_boot' CONF_ON_CLICK = 'on_click' CONF_ON_DOUBLE_CLICK = 'on_double_click' @@ -272,6 +287,7 @@ CONF_ON_TURN_OFF = 'on_turn_off' CONF_ON_TURN_ON = 'on_turn_on' CONF_ON_VALUE = 'on_value' CONF_ON_VALUE_RANGE = 'on_value_range' +CONF_ONE = 'one' CONF_OPEN_ACTION = 'open_action' CONF_OPEN_DURATION = 'open_duration' CONF_OPEN_ENDSTOP = 'open_endstop' @@ -283,11 +299,11 @@ CONF_OSCILLATION_OUTPUT = 'oscillation_output' CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic' CONF_OTA = 'ota' CONF_OUTPUT = 'output' -CONF_OUTPUTS = 'outputs' CONF_OUTPUT_ID = 'output_id' +CONF_OUTPUTS = 'outputs' CONF_OVERSAMPLING = 'oversampling' -CONF_PAGES = 'pages' CONF_PAGE_ID = 'page_id' +CONF_PAGES = 'pages' CONF_PANASONIC = 'panasonic' CONF_PASSWORD = 'password' CONF_PAYLOAD = 'payload' @@ -295,19 +311,20 @@ CONF_PAYLOAD_AVAILABLE = 'payload_available' CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' CONF_PHASE_BALANCER = 'phase_balancer' CONF_PIN = 'pin' -CONF_PINS = 'pins' CONF_PIN_A = 'pin_a' CONF_PIN_B = 'pin_b' CONF_PIN_C = 'pin_c' CONF_PIN_D = 'pin_d' +CONF_PINS = 'pins' CONF_PLATFORM = 'platform' CONF_PLATFORMIO_OPTIONS = 'platformio_options' -CONF_PM_10_0 = 'pm_10_0' CONF_PM_1_0 = 'pm_1_0' +CONF_PM_10_0 = 'pm_10_0' CONF_PM_2_5 = 'pm_2_5' CONF_PORT = 'port' CONF_POSITION = 'position' CONF_POWER = 'power' +CONF_POWER_FACTOR = 'power_factor' CONF_POWER_ON_VALUE = 'power_on_value' CONF_POWER_SAVE_MODE = 'power_save_mode' CONF_POWER_SUPPLY = 'power_supply' @@ -336,8 +353,8 @@ CONF_RESTORE_MODE = 'restore_mode' CONF_RESTORE_STATE = 'restore_state' CONF_RESTORE_VALUE = 'restore_value' CONF_RETAIN = 'retain' -CONF_RGBW = 'rgbw' CONF_RGB_ORDER = 'rgb_order' +CONF_RGBW = 'rgbw' CONF_RISING_EDGE = 'rising_edge' CONF_ROTATION = 'rotation' CONF_RS_PIN = 'rs_pin' @@ -349,7 +366,6 @@ CONF_RX_PIN = 'rx_pin' CONF_SAFE_MODE = 'safe_mode' CONF_SAMSUNG = 'samsung' CONF_SCAN = 'scan' -CONF_SCAN_INTERVAL = 'scan_interval' CONF_SCL = 'scl' CONF_SCL_PIN = 'scl_pin' CONF_SDA = 'sda' @@ -361,14 +377,14 @@ CONF_SEL_PIN = 'sel_pin' CONF_SEND_EVERY = 'send_every' CONF_SEND_FIRST_AT = 'send_first_at' CONF_SENSOR = 'sensor' -CONF_SENSORS = 'sensors' CONF_SENSOR_ID = 'sensor_id' +CONF_SENSORS = 'sensors' +CONF_SEQUENCE = 'sequence' CONF_SERVERS = 'servers' CONF_SERVICE = 'service' CONF_SERVICES = 'services' CONF_SETUP_MODE = 'setup_mode' CONF_SETUP_PRIORITY = 'setup_priority' -CONF_SEQUENCE = 'sequence' CONF_SHUNT_RESISTANCE = 'shunt_resistance' CONF_SHUNT_VOLTAGE = 'shunt_voltage' CONF_SHUTDOWN_MESSAGE = 'shutdown_message' @@ -381,6 +397,7 @@ CONF_SPEED = 'speed' CONF_SPEED_COMMAND_TOPIC = 'speed_command_topic' CONF_SPEED_STATE_TOPIC = 'speed_state_topic' CONF_SPI_ID = 'spi_id' +CONF_SPIKE_REJECTION = 'spike_rejection' CONF_SSID = 'ssid' CONF_SSL_FINGERPRINTS = 'ssl_fingerprints' CONF_STATE = 'state' @@ -391,6 +408,8 @@ CONF_STEP_PIN = 'step_pin' CONF_STOP = 'stop' CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' +CONF_SUPPORTS_COOL = 'supports_cool' +CONF_SUPPORTS_HEAT = 'supports_heat' CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TAG = 'tag' @@ -405,11 +424,13 @@ CONF_THEN = 'then' CONF_THRESHOLD = 'threshold' CONF_THROTTLE = 'throttle' CONF_TILT = 'tilt' +CONF_TILT_ACTION = 'tilt_action' +CONF_TILT_LAMBDA = 'tilt_lambda' CONF_TIME = 'time' +CONF_TIME_ID = 'time_id' CONF_TIMEOUT = 'timeout' CONF_TIMES = 'times' CONF_TIMEZONE = 'timezone' -CONF_TIME_ID = 'time_id' CONF_TIMING = 'timing' CONF_TO = 'to' CONF_TOLERANCE = 'tolerance' @@ -430,8 +451,8 @@ CONF_UNIQUE = 'unique' CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' CONF_UPDATE_INTERVAL = 'update_interval' CONF_UPDATE_ON_BOOT = 'update_on_boot' -CONF_USERNAME = 'username' CONF_USE_ADDRESS = 'use_address' +CONF_USERNAME = 'username' CONF_UUID = 'uuid' CONF_VALUE = 'value' CONF_VARIABLES = 'variables' @@ -445,11 +466,14 @@ CONF_WAIT_UNTIL = 'wait_until' CONF_WAKEUP_PIN = 'wakeup_pin' CONF_WARM_WHITE = 'warm_white' CONF_WARM_WHITE_COLOR_TEMPERATURE = 'warm_white_color_temperature' +CONF_WATCHDOG_THRESHOLD = 'watchdog_threshold' CONF_WHILE = 'while' CONF_WHITE = 'white' CONF_WIDTH = 'width' CONF_WIFI = 'wifi' CONF_WILL_MESSAGE = 'will_message' +CONF_WIND_DIRECTION_DEGREES = 'wind_direction_degrees' +CONF_WIND_SPEED = 'wind_speed' CONF_WINDOW_SIZE = 'window_size' CONF_ZERO = 'zero' @@ -457,8 +481,9 @@ ICON_ARROW_EXPAND_VERTICAL = 'mdi:arrow-expand-vertical' ICON_BATTERY = 'mdi:battery' ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' ICON_BRIGHTNESS_5 = 'mdi:brightness-5' -ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline' +ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' +ICON_CURRENT_AC = 'mdi:current-ac' ICON_EMPTY = '' ICON_FLASH = 'mdi:flash' ICON_FLOWER = 'mdi:flower' @@ -476,23 +501,29 @@ ICON_RESTART = 'mdi:restart' ICON_ROTATE_RIGHT = 'mdi:rotate-right' ICON_SCALE = 'mdi:scale' ICON_SCREEN_ROTATION = 'mdi:screen-rotation' -ICON_SIGNAL = 'mdi:signal' -ICON_WEATHER_SUNSET = 'mdi:weather-sunset' -ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' -ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up' +ICON_SIGN_DIRECTION = 'mdi:sign-direction' +ICON_SIGNAL = 'mdi: signal-distance-variant' +ICON_SIGNAL_DISTANCE_VARIANT = 'mdi:signal' ICON_THERMOMETER = 'mdi:thermometer' ICON_TIMER = 'mdi:timer' ICON_WATER_PERCENT = 'mdi:water-percent' +ICON_WEATHER_SUNSET = 'mdi:weather-sunset' +ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' +ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up' +ICON_WEATHER_WINDY = 'mdi:weather-windy' ICON_WIFI = 'mdi:wifi' UNIT_AMPERE = 'A' UNIT_CELSIUS = u'°C' UNIT_DECIBEL = 'dB' -UNIT_DEGREES = u'°' UNIT_DEGREE_PER_SECOND = u'°/s' +UNIT_DEGREES = u'°' UNIT_EMPTY = '' UNIT_HECTOPASCAL = 'hPa' +UNIT_HZ = 'hz' UNIT_KELVIN = 'K' +UNIT_KILOMETER = 'km' +UNIT_KILOMETER_PER_HOUR = 'km/h' UNIT_LUX = 'lx' UNIT_METER = 'm' UNIT_METER_PER_SECOND_SQUARED = u'm/s²' @@ -500,8 +531,8 @@ UNIT_MICROGRAMS_PER_CUBIC_METER = u'µg/m³' UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm' UNIT_MICROTESLA = u'µT' UNIT_OHM = u'Ω' -UNIT_PARTS_PER_MILLION = 'ppm' UNIT_PARTS_PER_BILLION = 'ppb' +UNIT_PARTS_PER_MILLION = 'ppm' UNIT_PERCENT = '%' UNIT_PULSES_PER_MINUTE = 'pulses/min' UNIT_SECOND = 's' diff --git a/esphome/core.py b/esphome/core.py index 7aaf6b2c70..82ce196cab 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -10,8 +10,8 @@ import re # pylint: disable=unused-import, wrong-import-order from typing import Any, Dict, List # noqa -from esphome.const import CONF_ARDUINO_VERSION, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI, \ - SOURCE_FILE_EXTENSIONS +from esphome.const import CONF_ARDUINO_VERSION, SOURCE_FILE_EXTENSIONS, \ + CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI from esphome.helpers import ensure_unique_string, is_hassio from esphome.py_compat import IS_PY2, integer_types, text_type, string_types from esphome.util import OrderedDict @@ -495,7 +495,7 @@ class EsphomeCore(object): # A list of statements to insert in the global block (includes and global variables) self.global_statements = [] # type: List[Statement] # A set of platformio libraries to add to the project - self.libraries = set() # type: Set[Library] + self.libraries = [] # type: List[Library] # A set of build flags to set in the platformio project self.build_flags = set() # type: Set[str] # A set of defines to set for the compile process in esphome/core/defines.h @@ -507,6 +507,8 @@ class EsphomeCore(object): self.loaded_integrations = set() # A set of component IDs to track what Component subclasses are declared self.component_ids = set() + # Whether ESPHome was started in verbose mode + self.verbose = False def reset(self): self.dashboard = False @@ -522,7 +524,7 @@ class EsphomeCore(object): self.variables = {} self.main_statements = [] self.global_statements = [] - self.libraries = set() + self.libraries = [] self.build_flags = set() self.defines = set() self.active_coroutines = {} @@ -539,6 +541,13 @@ class EsphomeCore(object): return None + @property + def comment(self): # type: () -> str + if CONF_COMMENT in self.config[CONF_ESPHOME]: + return self.config[CONF_ESPHOME][CONF_COMMENT] + + return None + def _add_active_coroutine(self, instance_id, obj): self.active_coroutines[instance_id] = obj @@ -666,8 +675,25 @@ class EsphomeCore(object): if not isinstance(library, Library): raise ValueError(u"Library {} must be instance of Library, not {}" u"".format(library, type(library))) - self.libraries.add(library) _LOGGER.debug("Adding library: %s", library) + for other in self.libraries[:]: + if other.name != library.name: + continue + if library.version is None: + # Other requirement is more specific + break + if other.version is None: + # Found more specific version requirement + self.libraries.remove(other) + continue + if other.version == library.version: + break + + raise ValueError(u"Version pinning failed! Libraries {} and {} " + u"requested with conflicting versions!" + u"".format(library, other)) + else: + self.libraries.append(library) return library def add_build_flag(self, build_flag): diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index f865643f57..2600ace218 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -33,10 +33,9 @@ void Application::setup() { for (uint32_t i = 0; i < this->components_.size(); i++) { Component *component = this->components_[i]; - if (component->is_failed()) - continue; - component->call_setup(); + component->call(); + this->scheduler.process_to_add(); if (component->can_proceed()) continue; @@ -45,10 +44,9 @@ void Application::setup() { do { uint32_t new_app_state = STATUS_LED_WARNING; + this->scheduler.call(); for (uint32_t j = 0; j <= i; j++) { - if (!this->components_[j]->is_failed()) { - this->components_[j]->call_loop(); - } + this->components_[j]->call(); new_app_state |= this->components_[j]->get_component_state(); this->app_state_ |= new_app_state; } @@ -63,20 +61,20 @@ void Application::setup() { void Application::loop() { uint32_t new_app_state = 0; const uint32_t start = millis(); + + this->scheduler.call(); for (Component *component : this->components_) { - if (!component->is_failed()) { - component->call_loop(); - } + component->call(); new_app_state |= component->get_component_state(); this->app_state_ |= new_app_state; this->feed_wdt(); } this->app_state_ = new_app_state; + const uint32_t end = millis(); if (end - start > 200) { - ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.1f s).", (end - start) / 1e3f); + ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.2f s).", (end - start) / 1e3f); ESP_LOGV(TAG, "Components should block for at most 20-30ms in loop()."); - ESP_LOGV(TAG, "This will become a warning soon."); } const uint32_t now = millis(); @@ -87,13 +85,19 @@ void Application::loop() { uint32_t delay_time = this->loop_interval_; if (now - this->last_loop_ < this->loop_interval_) delay_time = this->loop_interval_ - (now - this->last_loop_); + + uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time); + // next_schedule is max 0.5*delay_time + // otherwise interval=0 schedules result in constant looping with almost no sleep + next_schedule = std::max(next_schedule, delay_time / 2); + delay_time = std::min(next_schedule, delay_time); delay(delay_time); } this->last_loop_ = now; if (this->dump_config_at_ >= 0 && this->dump_config_at_ < this->components_.size()) { if (this->dump_config_at_ == 0) { - ESP_LOGI(TAG, "esphome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str()); + ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str()); } this->components_[this->dump_config_at_]->dump_config(); @@ -114,7 +118,7 @@ void ICACHE_RAM_ATTR HOT Application::feed_wdt() { LAST_FEED = now; #ifdef USE_STATUS_LED if (status_led::global_status_led != nullptr) { - status_led::global_status_led->call_loop(); + status_led::global_status_led->call(); } #endif } diff --git a/esphome/core/application.h b/esphome/core/application.h index c4cc1f27a8..2014b082e9 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -6,6 +6,7 @@ #include "esphome/core/preferences.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/scheduler.h" #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" @@ -39,7 +40,7 @@ class Application { void pre_setup(const std::string &name, const char *compilation_time) { this->name_ = name; this->compilation_time_ = compilation_time; - global_preferences.begin(this->name_); + global_preferences.begin(); } #ifdef USE_BINARY_SENSOR @@ -118,8 +119,12 @@ class Application { void safe_reboot(); void run_safe_shutdown_hooks() { - for (auto *comp : this->components_) + for (auto *comp : this->components_) { comp->on_safe_shutdown(); + } + for (auto *comp : this->components_) { + comp->on_shutdown(); + } } uint32_t get_app_state() const { return this->app_state_; } @@ -197,6 +202,8 @@ class Application { } #endif + Scheduler scheduler; + protected: friend Component; diff --git a/esphome/core/automation.h b/esphome/core/automation.h index ceed28e5b8..cbe96a749e 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -17,6 +17,15 @@ namespace esphome { #define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name) +#define TEMPLATABLE_STRING_VALUE_(name) \ + protected: \ + TemplatableStringValue<Ts...> name##_{}; \ +\ + public: \ + template<typename V> void set_##name(V name) { this->name##_ = name; } + +#define TEMPLATABLE_STRING_VALUE(name) TEMPLATABLE_STRING_VALUE_(name) + /** Base class for all automation conditions. * * @tparam Ts The template parameters to pass when executing. diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index fbd7439d70..0547fcbdd5 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -1,5 +1,3 @@ -#include <algorithm> - #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/esphal.h" @@ -45,109 +43,50 @@ void Component::setup() {} void Component::loop() {} void Component::set_interval(const std::string &name, uint32_t interval, std::function<void()> &&f) { // NOLINT - const uint32_t now = millis(); - // only put offset in lower half - uint32_t offset = 0; - if (interval != 0) - offset = (random_uint32() % interval) / 2; - ESP_LOGVV(TAG, "set_interval(name='%s', interval=%u, offset=%u)", name.c_str(), interval, offset); - - if (!name.empty()) { - this->cancel_interval(name); - } - struct TimeFunction function = { - .name = name, - .type = TimeFunction::INTERVAL, - .interval = interval, - .last_execution = now - interval - offset, - .f = std::move(f), - .remove = false, - }; - this->time_functions_.push_back(function); + App.scheduler.set_interval(this, name, interval, std::move(f)); } bool Component::cancel_interval(const std::string &name) { // NOLINT - return this->cancel_time_function_(name, TimeFunction::INTERVAL); + return App.scheduler.cancel_interval(this, name); } void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT - const uint32_t now = millis(); - ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%u)", name.c_str(), timeout); - - if (!name.empty()) { - this->cancel_timeout(name); - } - struct TimeFunction function = { - .name = name, - .type = TimeFunction::TIMEOUT, - .interval = timeout, - .last_execution = now, - .f = std::move(f), - .remove = false, - }; - this->time_functions_.push_back(function); + return App.scheduler.set_timeout(this, name, timeout, std::move(f)); } bool Component::cancel_timeout(const std::string &name) { // NOLINT - return this->cancel_time_function_(name, TimeFunction::TIMEOUT); + return App.scheduler.cancel_timeout(this, name); } -void Component::call_loop() { - this->loop_internal_(); - this->loop(); -} +void Component::call_loop() { this->loop(); } -bool Component::cancel_time_function_(const std::string &name, TimeFunction::Type type) { - // NOLINTNEXTLINE - for (auto iter = this->time_functions_.begin(); iter != this->time_functions_.end(); iter++) { - if (!iter->remove && iter->name == name && iter->type == type) { - ESP_LOGVV(TAG, "Removing old time function %s.", iter->name.c_str()); - iter->remove = true; - return true; - } - } - return false; -} -void Component::call_setup() { - this->setup_internal_(); - this->setup(); -} +void Component::call_setup() { this->setup(); } uint32_t Component::get_component_state() const { return this->component_state_; } -void Component::loop_internal_() { - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_LOOP; - - for (unsigned int i = 0; i < this->time_functions_.size(); i++) { // NOLINT - const uint32_t now = millis(); - TimeFunction *tf = &this->time_functions_[i]; - if (tf->should_run(now)) { -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - const char *type = - tf->type == TimeFunction::INTERVAL ? "interval" : (tf->type == TimeFunction::TIMEOUT ? "timeout" : "defer"); - ESP_LOGVV(TAG, "Running %s '%s':%u with interval=%u last_execution=%u (now=%u)", type, tf->name.c_str(), i, - tf->interval, tf->last_execution, now); -#endif - - tf->f(); - // The vector might have reallocated due to new items - tf = &this->time_functions_[i]; - - if (tf->type == TimeFunction::INTERVAL && tf->interval != 0) { - const uint32_t amount = (now - tf->last_execution) / tf->interval; - tf->last_execution += (amount * tf->interval); - } else if (tf->type == TimeFunction::DEFER || tf->type == TimeFunction::TIMEOUT) { - tf->remove = true; - } - } +void Component::call() { + uint32_t state = this->component_state_ & COMPONENT_STATE_MASK; + switch (state) { + case COMPONENT_STATE_CONSTRUCTION: + // State Construction: Call setup and set state to setup + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_SETUP; + this->call_setup(); + break; + case COMPONENT_STATE_SETUP: + // State setup: Call first loop and set state to loop + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_LOOP; + this->call_loop(); + break; + case COMPONENT_STATE_LOOP: + // State loop: Call loop + this->call_loop(); + break; + case COMPONENT_STATE_FAILED: + // State failed: Do nothing + break; + default: + break; } - - this->time_functions_.erase(std::remove_if(this->time_functions_.begin(), this->time_functions_.end(), - [](const TimeFunction &tf) -> bool { return tf.remove; }), - this->time_functions_.end()); -} -void Component::setup_internal_() { - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_SETUP; } void Component::mark_failed() { ESP_LOGE(TAG, "Component was marked as failed."); @@ -155,29 +94,20 @@ void Component::mark_failed() { this->component_state_ |= COMPONENT_STATE_FAILED; this->status_set_error(); } -void Component::defer(std::function<void()> &&f) { this->defer("", std::move(f)); } // NOLINT -bool Component::cancel_defer(const std::string &name) { // NOLINT - return this->cancel_time_function_(name, TimeFunction::DEFER); +void Component::defer(std::function<void()> &&f) { // NOLINT + App.scheduler.set_timeout(this, "", 0, std::move(f)); +} +bool Component::cancel_defer(const std::string &name) { // NOLINT + return App.scheduler.cancel_timeout(this, name); } void Component::defer(const std::string &name, std::function<void()> &&f) { // NOLINT - if (!name.empty()) { - this->cancel_defer(name); - } - struct TimeFunction function = { - .name = name, - .type = TimeFunction::DEFER, - .interval = 0, - .last_execution = 0, - .f = std::move(f), - .remove = false, - }; - this->time_functions_.push_back(function); + App.scheduler.set_timeout(this, name, 0, std::move(f)); } void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // NOLINT - this->set_timeout("", timeout, std::move(f)); + App.scheduler.set_timeout(this, "", timeout, std::move(f)); } void Component::set_interval(uint32_t interval, std::function<void()> &&f) { // NOLINT - this->set_interval("", interval, std::move(f)); + App.scheduler.set_interval(this, "", interval, std::move(f)); } bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::can_proceed() { return true; } @@ -203,16 +133,15 @@ void Component::status_momentary_error(const std::string &name, uint32_t length) } void Component::dump_config() {} float Component::get_actual_setup_priority() const { - return this->setup_priority_override_.value_or(this->get_setup_priority()); + if (isnan(this->setup_priority_override_)) + return this->get_setup_priority(); + return this->setup_priority_override_; } void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {} void PollingComponent::call_setup() { - // Call component internal setup. - this->setup_internal_(); - // Let the polling component subclass setup their HW. this->setup(); @@ -240,12 +169,4 @@ void Nameable::calc_object_id_() { } uint32_t Nameable::get_object_id_hash() { return this->object_id_hash_; } -bool Component::TimeFunction::should_run(uint32_t now) const { - if (this->remove) - return false; - if (this->type == DEFER) - return true; - return this->interval != 4294967295UL && now - this->last_execution > this->interval; -} - } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 60f306ede4..c8e05cc252 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -2,7 +2,7 @@ #include <string> #include <functional> -#include <vector> +#include "Arduino.h" #include "esphome/core/optional.h" @@ -91,18 +91,7 @@ class Component { */ virtual float get_loop_priority() const; - /** Public loop() functions. These will be called by the Application instance. - * - * Note: This should normally not be overriden, unless you know what you're doing. - * They're basically to make creating custom components easier. For example the - * SensorComponent can override these methods to not have the user call some super - * methods within their custom sensors. These methods should ALWAYS call the loop_internal() - * and setup_internal() methods. - * - * Basically, it handles stuff like interval/timeout functions and eventually calls loop(). - */ - virtual void call_loop(); - virtual void call_setup(); + void call(); virtual void on_shutdown() {} virtual void on_safe_shutdown() {} @@ -138,6 +127,8 @@ class Component { void status_momentary_error(const std::string &name, uint32_t length = 5000); protected: + virtual void call_loop(); + virtual void call_setup(); /** Set an interval function with a unique name. Empty name means no cancelling possible. * * This will call f every interval ms. Can be cancelled via CancelInterval(). @@ -204,34 +195,8 @@ class Component { /// Cancel a defer callback using the specified name, name must not be empty. bool cancel_defer(const std::string &name); // NOLINT - void loop_internal_(); - void setup_internal_(); - - /// Internal struct for storing timeout/interval functions. - struct TimeFunction { - std::string name; ///< The name/id of this TimeFunction. - enum Type { TIMEOUT, INTERVAL, DEFER } type; ///< The type of this TimeFunction. Either TIMEOUT, INTERVAL or DEFER. - uint32_t interval; ///< The interval/timeout of this function. - /// The last execution for interval functions and the time, SetInterval was called, for timeout functions. - uint32_t last_execution; - std::function<void()> f; ///< The function (or callback) itself. - bool remove; - - bool should_run(uint32_t now) const; - }; - - /// Cancel an only time function. If name is empty, won't do anything. - bool cancel_time_function_(const std::string &name, TimeFunction::Type type); - - /** Storage for interval/timeout functions. - * - * Intentionally a vector despite its map-like nature, because of the - * memory overhead. - */ - std::vector<TimeFunction> time_functions_; - uint32_t component_state_{0x0000}; ///< State of this component. - optional<float> setup_priority_override_; + float setup_priority_override_{NAN}; }; /** This class simplifies creating components that periodically check a state. diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c52a4390dc..fe10a42baa 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -22,3 +22,4 @@ #endif #define USE_TIME #define USE_DEEP_SLEEP +#define USE_CAPTIVE_PORTAL diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp index f0749894c0..13d54e726d 100644 --- a/esphome/core/esphal.cpp +++ b/esphome/core/esphal.cpp @@ -148,7 +148,7 @@ void ICACHE_RAM_ATTR HOT GPIOPin::digital_write(bool value) { } #endif } -void ISRInternalGPIOPin::digital_write(bool value) { +void ICACHE_RAM_ATTR HOT ISRInternalGPIOPin::digital_write(bool value) { #ifdef ARDUINO_ARCH_ESP8266 if (this->pin_ != 16) { if (value != this->inverted_) { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c65ca919ba..6d6aa80b66 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -314,4 +314,20 @@ std::array<uint8_t, 2> decode_uint16(uint16_t value) { return {msb, lsb}; } +std::string hexencode(const uint8_t *data, uint32_t len) { + char buf[20]; + std::string res; + for (size_t i = 0; i < len; i++) { + if (i + 1 != len) { + sprintf(buf, "%02X.", data[i]); + } else { + sprintf(buf, "%02X ", data[i]); + } + res += buf; + } + sprintf(buf, "(%u)", len); + res += buf; + return res; +} + } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index d21cb85b7d..88f0d587e5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -4,6 +4,7 @@ #include <functional> #include <vector> #include <memory> +#include <type_traits> #include "esphome/core/optional.h" #include "esphome/core/esphal.h" @@ -155,11 +156,19 @@ enum ParseOnOffState { ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr); +// Encode raw data to a human-readable string (for debugging) +std::string hexencode(const uint8_t *data, uint32_t len); + // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 template<int...> struct seq {}; // NOLINT template<int N, int... S> struct gens : gens<N - 1, N - 1, S...> {}; // NOLINT template<int... S> struct gens<0, S...> { using type = seq<S...>; }; // NOLINT +template<bool B, class T = void> using enable_if_t = typename std::enable_if<B, T>::type; + +template<typename T, enable_if_t<!std::is_pointer<T>::value, int> = 0> T id(T value) { return value; } +template<typename T, enable_if_t<std::is_pointer<T *>::value, int> = 0> T &id(T *value) { return *value; } + template<typename... X> class CallbackManager; /** Simple helper class to allow having multiple subscribers to a signal. @@ -192,8 +201,6 @@ struct is_callable // NOLINT static constexpr auto value = decltype(test<T>(nullptr))::value; // NOLINT }; -template<bool B, class T = void> using enable_if_t = typename std::enable_if<B, T>::type; - template<typename T, typename... X> class TemplatableValue { public: TemplatableValue() : type_(EMPTY) {} @@ -239,6 +246,18 @@ template<typename T, typename... X> class TemplatableValue { std::function<T(X...)> f_; }; +template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { + public: + TemplatableStringValue() : TemplatableValue<std::string, X...>() {} + + template<typename F, enable_if_t<!is_callable<F, X...>::value, int> = 0> + TemplatableStringValue(F value) : TemplatableValue<std::string, X...>(value) {} + + template<typename F, enable_if_t<is_callable<F, X...>::value, int> = 0> + TemplatableStringValue(F f) + : TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {} +}; + void delay_microseconds_accurate(uint32_t usec); template<typename T> class Deduplicator { diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 8adaebe5b5..15d49c0038 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -1,6 +1,6 @@ -#include "esphome/core/log.h" -#include "esphome/core/defines.h" -#include "esphome/core/helpers.h" +#include "log.h" +#include "defines.h" +#include "helpers.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -8,60 +8,65 @@ namespace esphome { -int HOT esp_log_printf_(int level, const char *tag, const char *format, ...) { // NOLINT +void HOT esp_log_printf_(int level, const char *tag, int line, const char *format, ...) { // NOLINT va_list arg; va_start(arg, format); - int ret = esp_log_vprintf_(level, tag, format, arg); + esp_log_vprintf_(level, tag, line, format, arg); va_end(arg); - return ret; } #ifdef USE_STORE_LOG_STR_IN_FLASH -int HOT esp_log_printf_(int level, const char *tag, const __FlashStringHelper *format, ...) { +void HOT esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...) { va_list arg; va_start(arg, format); - int ret = esp_log_vprintf_(level, tag, format, arg); + esp_log_vprintf_(level, tag, line, format, arg); va_end(arg); - return ret; - return 0; } #endif -int HOT esp_log_vprintf_(int level, const char *tag, const char *format, va_list args) { // NOLINT +void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT #ifdef USE_LOGGER auto *log = logger::global_logger; if (log == nullptr) - return 0; + return; - return log->log_vprintf_(level, tag, format, args); -#else - return 0; + log->log_vprintf_(level, tag, line, format, args); #endif } #ifdef USE_STORE_LOG_STR_IN_FLASH -int HOT esp_log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args) { // NOLINT +void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, + va_list args) { // NOLINT #ifdef USE_LOGGER auto *log = logger::global_logger; if (log == nullptr) - return 0; + return; - return log->log_vprintf_(level, tag, format, args); -#else - return 0; + log->log_vprintf_(level, tag, line, format, args); #endif } #endif +#ifdef ARDUINO_ARCH_ESP32 int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT #ifdef USE_LOGGER auto *log = logger::global_logger; if (log == nullptr) return 0; - return log->log_vprintf_(log->get_global_log_level(), "", format, args); -#else - return 0; + size_t len = strlen(format); + if (format[len - 1] == '\n') { + // Remove trailing newline from format + // Use locally stored + static std::string FORMAT_COPY; + FORMAT_COPY.clear(); + FORMAT_COPY.insert(0, format, len - 1); + format = FORMAT_COPY.c_str(); + } + + log->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args); #endif + return 0; } +#endif } // namespace esphome diff --git a/esphome/core/log.h b/esphome/core/log.h index 4e4d178b96..361fbe1182 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -20,9 +20,10 @@ namespace esphome { #define ESPHOME_LOG_LEVEL_ERROR 1 #define ESPHOME_LOG_LEVEL_WARN 2 #define ESPHOME_LOG_LEVEL_INFO 3 -#define ESPHOME_LOG_LEVEL_DEBUG 4 -#define ESPHOME_LOG_LEVEL_VERBOSE 5 -#define ESPHOME_LOG_LEVEL_VERY_VERBOSE 6 +#define ESPHOME_LOG_LEVEL_CONFIG 4 +#define ESPHOME_LOG_LEVEL_DEBUG 5 +#define ESPHOME_LOG_LEVEL_VERBOSE 6 +#define ESPHOME_LOG_LEVEL_VERY_VERBOSE 7 #ifndef ESPHOME_LOG_LEVEL #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_DEBUG @@ -43,38 +44,30 @@ namespace esphome { #define ESPHOME_LOG_COLOR(COLOR) "\033[0;" COLOR "m" #define ESPHOME_LOG_BOLD(COLOR) "\033[1;" COLOR "m" - -#define ESPHOME_LOG_COLOR_E ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED) -#define ESPHOME_LOG_COLOR_W ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW) -#define ESPHOME_LOG_COLOR_I ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN) -#define ESPHOME_LOG_COLOR_C ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA) -#define ESPHOME_LOG_COLOR_D ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN) -#define ESPHOME_LOG_COLOR_V ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY) -#define ESPHOME_LOG_COLOR_VV ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE) #define ESPHOME_LOG_RESET_COLOR "\033[0m" -int esp_log_printf_(int level, const char *tag, const char *format, ...) // NOLINT - __attribute__((format(printf, 3, 4))); +void esp_log_printf_(int level, const char *tag, int line, const char *format, ...) // NOLINT + __attribute__((format(printf, 4, 5))); #ifdef USE_STORE_LOG_STR_IN_FLASH -int esp_log_printf_(int level, const char *tag, const __FlashStringHelper *format, ...); +void esp_log_printf_(int level, const char *tag, int line, const __FlashStringHelper *format, ...); #endif -int esp_log_vprintf_(int level, const char *tag, const char *format, va_list args); // NOLINT +void esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH -int esp_log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args); +void esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); #endif +#ifdef ARDUINO_ARCH_ESP32 int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT +#endif #ifdef USE_STORE_LOG_STR_IN_FLASH -#define ESPHOME_LOG_FORMAT(tag, letter, format) \ - F(ESPHOME_LOG_COLOR_##letter "[" #letter "][%s:%03u]: " format ESPHOME_LOG_RESET_COLOR), tag, __LINE__ +#define ESPHOME_LOG_FORMAT(format) F(format) #else -#define ESPHOME_LOG_FORMAT(tag, letter, format) \ - ESPHOME_LOG_COLOR_##letter "[" #letter "][%s:%03u]: " format ESPHOME_LOG_RESET_COLOR, tag, __LINE__ +#define ESPHOME_LOG_FORMAT(format) format #endif #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #define esph_log_vv(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, ESPHOME_LOG_FORMAT(tag, VV, format), ##__VA_ARGS__) + esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_VERY_VERBOSE #else @@ -83,7 +76,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #define esph_log_v(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, ESPHOME_LOG_FORMAT(tag, V, format), ##__VA_ARGS__) + esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_VERBOSE #else @@ -92,22 +85,20 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG #define esph_log_d(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, ESPHOME_LOG_FORMAT(tag, D, format), ##__VA_ARGS__) - + esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define esph_log_config(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, ESPHOME_LOG_FORMAT(tag, C, format), ##__VA_ARGS__) + esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_DEBUG #define ESPHOME_LOG_HAS_CONFIG #else #define esph_log_d(tag, format, ...) - #define esph_log_config(tag, format, ...) #endif #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO #define esph_log_i(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, ESPHOME_LOG_FORMAT(tag, I, format), ##__VA_ARGS__) + esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_INFO #else @@ -116,7 +107,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN #define esph_log_w(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, ESPHOME_LOG_FORMAT(tag, W, format), ##__VA_ARGS__) + esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_WARN #else @@ -125,7 +116,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR #define esph_log_e(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, ESPHOME_LOG_FORMAT(tag, E, format), ##__VA_ARGS__) + esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_ERROR #else diff --git a/esphome/core/preferences.cpp b/esphome/core/preferences.cpp index 65140bbdc8..2329ed34f5 100644 --- a/esphome/core/preferences.cpp +++ b/esphome/core/preferences.cpp @@ -1,20 +1,25 @@ #include "esphome/core/preferences.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/application.h" -#ifdef USE_ESP8266_PREFERENCES_FLASH +#ifdef ARDUINO_ARCH_ESP8266 extern "C" { #include "spi_flash.h" } #endif +#ifdef ARDUINO_ARCH_ESP32 +#include "nvs.h" +#include "nvs_flash.h" +#endif namespace esphome { static const char *TAG = "preferences"; -ESPPreferenceObject::ESPPreferenceObject() : rtc_offset_(0), length_words_(0), type_(0), data_(nullptr) {} -ESPPreferenceObject::ESPPreferenceObject(size_t rtc_offset, size_t length, uint32_t type) - : rtc_offset_(rtc_offset), length_words_(length), type_(type) { +ESPPreferenceObject::ESPPreferenceObject() : offset_(0), length_words_(0), type_(0), data_(nullptr) {} +ESPPreferenceObject::ESPPreferenceObject(size_t offset, size_t length, uint32_t type) + : offset_(offset), length_words_(length), type_(type) { this->data_ = new uint32_t[this->length_words_ + 1]; for (uint32_t i = 0; i < this->length_words_ + 1; i++) this->data_[i] = 0; @@ -29,7 +34,7 @@ bool ESPPreferenceObject::load_() { bool valid = this->data_[this->length_words_] == this->calculate_crc_(); - ESP_LOGVV(TAG, "LOAD %u: valid=%s, 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->rtc_offset_, // NOLINT + ESP_LOGVV(TAG, "LOAD %u: valid=%s, 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->offset_, // NOLINT YESNO(valid), this->data_[0], this->data_[1], this->type_, this->calculate_crc_()); return valid; } @@ -42,17 +47,23 @@ bool ESPPreferenceObject::save_() { this->data_[this->length_words_] = this->calculate_crc_(); if (!this->save_internal_()) return false; - ESP_LOGVV(TAG, "SAVE %u: 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->rtc_offset_, // NOLINT + ESP_LOGVV(TAG, "SAVE %u: 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->offset_, // NOLINT this->data_[0], this->data_[1], this->type_, this->calculate_crc_()); return true; } #ifdef ARDUINO_ARCH_ESP8266 -#define ESP_RTC_USER_MEM_START 0x60001200 +static const uint32_t ESP_RTC_USER_MEM_START = 0x60001200; #define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) -#define ESP_RTC_USER_MEM_SIZE_WORDS 128 -#define ESP_RTC_USER_MEM_SIZE_BYTES ESP_RTC_USER_MEM_SIZE_WORDS * 4 +static const uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; +static const uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; + +#ifdef USE_ESP8266_PREFERENCES_FLASH +static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; +#else +static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; +#endif static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { @@ -62,9 +73,7 @@ static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { return true; } -#ifdef USE_ESP8266_PREFERENCES_FLASH -static bool esp8266_preferences_modified = false; -#endif +static bool esp8266_flash_dirty = false; static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) { if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { @@ -75,29 +84,24 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) { } auto *ptr = &ESP_RTC_USER_MEM[index]; -#ifdef USE_ESP8266_PREFERENCES_FLASH - if (*ptr != value) { - esp8266_preferences_modified = true; - } -#endif *ptr = value; return true; } -#ifdef USE_ESP8266_PREFERENCES_FLASH extern "C" uint32_t _SPIFFS_end; -static const uint32_t get_esp8266_flash_sector() { return (uint32_t(&_SPIFFS_end) - 0x40200000) / SPI_FLASH_SEC_SIZE; } +static const uint32_t get_esp8266_flash_sector() { + union { + uint32_t *ptr; + uint32_t uint; + } data{}; + data.ptr = &_SPIFFS_end; + return (data.uint - 0x40200000) / SPI_FLASH_SEC_SIZE; +} static const uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; } -static void load_esp8266_flash() { - ESP_LOGVV(TAG, "Loading preferences from flash..."); - disable_interrupts(); - spi_flash_read(get_esp8266_flash_address(), ESP_RTC_USER_MEM, ESP_RTC_USER_MEM_SIZE_BYTES); - enable_interrupts(); -} -static void save_esp8266_flash() { - if (!esp8266_preferences_modified) +void ESPPreferences::save_esp8266_flash_() { + if (!esp8266_flash_dirty) return; ESP_LOGVV(TAG, "Saving preferences to flash..."); @@ -109,31 +113,53 @@ static void save_esp8266_flash() { return; } - auto write_res = spi_flash_write(get_esp8266_flash_address(), ESP_RTC_USER_MEM, ESP_RTC_USER_MEM_SIZE_BYTES); + auto write_res = spi_flash_write(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4); enable_interrupts(); if (write_res != SPI_FLASH_RESULT_OK) { ESP_LOGV(TAG, "Write ESP8266 flash failed!"); return; } - esp8266_preferences_modified = false; + esp8266_flash_dirty = false; } -#endif bool ESPPreferenceObject::save_internal_() { + if (this->in_flash_) { + for (uint32_t i = 0; i <= this->length_words_; i++) { + uint32_t j = this->offset_ + i; + if (j >= ESP8266_FLASH_STORAGE_SIZE) + return false; + uint32_t v = this->data_[i]; + uint32_t *ptr = &global_preferences.flash_storage_[j]; + if (*ptr != v) + esp8266_flash_dirty = true; + *ptr = v; + } + global_preferences.save_esp8266_flash_(); + return true; + } + for (uint32_t i = 0; i <= this->length_words_; i++) { - if (!esp_rtc_user_mem_write(this->rtc_offset_ + i, this->data_[i])) + if (!esp_rtc_user_mem_write(this->offset_ + i, this->data_[i])) return false; } -#ifdef USE_ESP8266_PREFERENCES_FLASH - save_esp8266_flash(); -#endif return true; } bool ESPPreferenceObject::load_internal_() { + if (this->in_flash_) { + for (uint32_t i = 0; i <= this->length_words_; i++) { + uint32_t j = this->offset_ + i; + if (j >= ESP8266_FLASH_STORAGE_SIZE) + return false; + this->data_[i] = global_preferences.flash_storage_[j]; + } + + return true; + } + for (uint32_t i = 0; i <= this->length_words_; i++) { - if (!esp_rtc_user_mem_read(this->rtc_offset_ + i, &this->data_[i])) + if (!esp_rtc_user_mem_read(this->offset_ + i, &this->data_[i])) return false; } return true; @@ -144,13 +170,26 @@ ESPPreferences::ESPPreferences() // which will be reset each time OTA occurs : current_offset_(0) {} -void ESPPreferences::begin(const std::string &name) { -#ifdef USE_ESP8266_PREFERENCES_FLASH - load_esp8266_flash(); -#endif +void ESPPreferences::begin() { + this->flash_storage_ = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; + ESP_LOGVV(TAG, "Loading preferences from flash..."); + disable_interrupts(); + spi_flash_read(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4); + enable_interrupts(); } -ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type) { +ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) { + if (in_flash) { + uint32_t start = this->current_flash_offset_; + uint32_t end = start + length + 1; + if (end > ESP8266_FLASH_STORAGE_SIZE) + return {}; + auto pref = ESPPreferenceObject(start, length, type); + pref.in_flash_ = true; + this->current_flash_offset_ = end; + return pref; + } + uint32_t start = this->current_offset_; uint32_t end = start + length + 1; bool in_normal = start < 96; @@ -165,7 +204,7 @@ ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type if (end > 128) { // Doesn't fit in data, return uninitialized preference obj. - return ESPPreferenceObject(); + return {}; } uint32_t rtc_offset; @@ -185,35 +224,67 @@ bool ESPPreferences::is_prevent_write() { return this->prevent_write_; } #ifdef ARDUINO_ARCH_ESP32 bool ESPPreferenceObject::save_internal_() { + if (global_preferences.nvs_handle_ == 0) + return false; + char key[32]; - sprintf(key, "%u", this->rtc_offset_); + sprintf(key, "%u", this->offset_); uint32_t len = (this->length_words_ + 1) * 4; - size_t ret = global_preferences.preferences_.putBytes(key, this->data_, len); - if (ret != len) { - ESP_LOGV(TAG, "putBytes failed!"); + esp_err_t err = nvs_set_blob(global_preferences.nvs_handle_, key, this->data_, len); + if (err) { + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", key, len, esp_err_to_name(err)); + return false; + } + err = nvs_commit(global_preferences.nvs_handle_); + if (err) { + ESP_LOGV(TAG, "nvs_commit('%s', len=%u) failed: %s", key, len, esp_err_to_name(err)); return false; } return true; } bool ESPPreferenceObject::load_internal_() { + if (global_preferences.nvs_handle_ == 0) + return false; + char key[32]; - sprintf(key, "%u", this->rtc_offset_); + sprintf(key, "%u", this->offset_); uint32_t len = (this->length_words_ + 1) * 4; - size_t ret = global_preferences.preferences_.getBytes(key, this->data_, len); - if (ret != len) { - ESP_LOGV(TAG, "getBytes failed!"); + + uint32_t actual_len; + esp_err_t err = nvs_get_blob(global_preferences.nvs_handle_, key, nullptr, &actual_len); + if (err) { + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key, esp_err_to_name(err)); + return false; + } + if (actual_len != len) { + ESP_LOGVV(TAG, "NVS length does not match. Assuming key changed (%u!=%u)", actual_len, len); + return false; + } + err = nvs_get_blob(global_preferences.nvs_handle_, key, this->data_, &len); + if (err) { + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key, esp_err_to_name(err)); return false; } return true; } ESPPreferences::ESPPreferences() : current_offset_(0) {} -void ESPPreferences::begin(const std::string &name) { - const std::string key = truncate_string(name, 15); - ESP_LOGV(TAG, "Opening preferences with key '%s'", key.c_str()); - this->preferences_.begin(key.c_str()); +void ESPPreferences::begin() { + auto ns = truncate_string(App.get_name(), 15); + esp_err_t err = nvs_open(ns.c_str(), NVS_READWRITE, &this->nvs_handle_); + if (err) { + ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS...", esp_err_to_name(err)); + nvs_flash_deinit(); + nvs_flash_erase(); + nvs_flash_init(); + + err = nvs_open(ns.c_str(), NVS_READWRITE, &this->nvs_handle_); + if (err) { + this->nvs_handle_ = 0; + } + } } -ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type) { +ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) { auto pref = ESPPreferenceObject(this->current_offset_, length, type); this->current_offset_++; return pref; diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h index 0574c757fb..bfea4c2336 100644 --- a/esphome/core/preferences.h +++ b/esphome/core/preferences.h @@ -2,10 +2,6 @@ #include <string> -#ifdef ARDUINO_ARCH_ESP32 -#include <Preferences.h> -#endif - #include "esphome/core/esphal.h" #include "esphome/core/defines.h" @@ -14,7 +10,7 @@ namespace esphome { class ESPPreferenceObject { public: ESPPreferenceObject(); - ESPPreferenceObject(size_t rtc_offset, size_t length, uint32_t type); + ESPPreferenceObject(size_t offset, size_t length, uint32_t type); template<typename T> bool save(T *src); @@ -23,6 +19,8 @@ class ESPPreferenceObject { bool is_initialized() const; protected: + friend class ESPPreferences; + bool save_(); bool load_(); bool save_internal_(); @@ -30,18 +28,33 @@ class ESPPreferenceObject { uint32_t calculate_crc_() const; - size_t rtc_offset_; + size_t offset_; size_t length_words_; uint32_t type_; uint32_t *data_; +#ifdef ARDUINO_ARCH_ESP8266 + bool in_flash_{false}; +#endif }; +#ifdef ARDUINO_ARCH_ESP8266 +#ifdef USE_ESP8266_PREFERENCES_FLASH +static bool DEFAULT_IN_FLASH = true; +#else +static bool DEFAULT_IN_FLASH = false; +#endif +#endif + +#ifdef ARDUINO_ARCH_ESP32 +static bool DEFAULT_IN_FLASH = true; +#endif + class ESPPreferences { public: ESPPreferences(); - void begin(const std::string &name); - ESPPreferenceObject make_preference(size_t length, uint32_t type); - template<typename T> ESPPreferenceObject make_preference(uint32_t type); + void begin(); + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash = DEFAULT_IN_FLASH); + template<typename T> ESPPreferenceObject make_preference(uint32_t type, bool in_flash = DEFAULT_IN_FLASH); #ifdef ARDUINO_ARCH_ESP8266 /** On the ESP8266, we can't override the first 128 bytes during OTA uploads @@ -60,17 +73,20 @@ class ESPPreferences { uint32_t current_offset_; #ifdef ARDUINO_ARCH_ESP32 - Preferences preferences_; + uint32_t nvs_handle_; #endif #ifdef ARDUINO_ARCH_ESP8266 + void save_esp8266_flash_(); bool prevent_write_{false}; + uint32_t *flash_storage_; + uint32_t current_flash_offset_; #endif }; extern ESPPreferences global_preferences; -template<typename T> ESPPreferenceObject ESPPreferences::make_preference(uint32_t type) { - return this->make_preference((sizeof(T) + 3) / 4, type); +template<typename T> ESPPreferenceObject ESPPreferences::make_preference(uint32_t type, bool in_flash) { + return this->make_preference((sizeof(T) + 3) / 4, type, in_flash); } template<typename T> bool ESPPreferenceObject::save(T *src) { diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp new file mode 100644 index 0000000000..cc4331b38e --- /dev/null +++ b/esphome/core/scheduler.cpp @@ -0,0 +1,246 @@ +#include "scheduler.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include <algorithm> + +namespace esphome { + +static const char *TAG = "scheduler"; + +static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; + +// Uncomment to debug scheduler +// #define ESPHOME_DEBUG_SCHEDULER + +void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, + std::function<void()> &&func) { + const uint32_t now = this->millis_(); + + if (!name.empty()) + this->cancel_timeout(component, name); + + if (timeout == SCHEDULER_DONT_RUN) + return; + + ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%u)", name.c_str(), timeout); + + auto item = make_unique<SchedulerItem>(); + item->component = component; + item->name = name; + item->type = SchedulerItem::TIMEOUT; + item->timeout = timeout; + item->last_execution = now; + item->last_execution_major = this->millis_major_; + item->f = std::move(func); + item->remove = false; + this->push_(std::move(item)); +} +bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { + return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); +} +void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, + std::function<void()> &&func) { + const uint32_t now = this->millis_(); + + if (!name.empty()) + this->cancel_interval(component, name); + + if (interval == SCHEDULER_DONT_RUN) + return; + + // only put offset in lower half + uint32_t offset = 0; + if (interval != 0) + offset = (random_uint32() % interval) / 2; + + ESP_LOGVV(TAG, "set_interval(name='%s', interval=%u, offset=%u)", name.c_str(), interval, offset); + + auto item = make_unique<SchedulerItem>(); + item->component = component; + item->name = name; + item->type = SchedulerItem::INTERVAL; + item->interval = interval; + item->last_execution = now - offset - interval; + item->last_execution_major = this->millis_major_; + if (item->last_execution > now) + item->last_execution_major--; + item->f = std::move(func); + item->remove = false; + this->push_(std::move(item)); +} +bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { + return this->cancel_item_(component, name, SchedulerItem::INTERVAL); +} +optional<uint32_t> HOT Scheduler::next_schedule_in() { + if (this->empty_()) + return {}; + auto &item = this->items_[0]; + const uint32_t now = this->millis_(); + uint32_t next_time = item->last_execution + item->interval; + if (next_time < now) + return 0; + return next_time - now; +} +void ICACHE_RAM_ATTR HOT Scheduler::call() { + const uint32_t now = this->millis_(); + this->process_to_add(); + +#ifdef ESPHOME_DEBUG_SCHEDULER + static uint32_t last_print = 0; + + if (now - last_print > 2000) { + last_print = now; + std::vector<std::unique_ptr<SchedulerItem>> old_items; + ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now); + while (!this->empty_()) { + auto item = std::move(this->items_[0]); + const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout"; + ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", type, item->name.c_str(), + item->interval, item->last_execution, item->last_execution_major, item->next_execution(), + item->next_execution_major()); + + this->pop_raw_(); + old_items.push_back(std::move(item)); + } + ESP_LOGVV(TAG, "\n"); + this->items_ = std::move(old_items); + } +#endif // ESPHOME_DEBUG_SCHEDULER + + while (!this->empty_()) { + // use scoping to indicate visibility of `item` variable + { + // Don't copy-by value yet + auto &item = this->items_[0]; + if ((now - item->last_execution) < item->interval) + // Not reached timeout yet, done for this call + break; + uint8_t major = item->next_execution_major(); + if (this->millis_major_ - major > 1) + break; + + // Don't run on failed components + if (item->component != nullptr && item->component->is_failed()) { + this->pop_raw_(); + continue; + } + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout"; + ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", type, item->name.c_str(), + item->interval, item->last_execution, now); +#endif + + // Warning: During f(), a lot of stuff can happen, including: + // - timeouts/intervals get added, potentially invalidating vector pointers + // - timeouts/intervals get cancelled + item->f(); + } + + { + // new scope, item from before might have been moved in the vector + auto item = std::move(this->items_[0]); + + // Only pop after function call, this ensures we were reachable + // during the function call and know if we were cancelled. + this->pop_raw_(); + + if (item->remove) { + // We were removed/cancelled in the function call, stop + continue; + } + + if (item->type == SchedulerItem::INTERVAL) { + if (item->interval != 0) { + const uint32_t before = item->last_execution; + const uint32_t amount = (now - item->last_execution) / item->interval; + item->last_execution += amount * item->interval; + if (item->last_execution < before) + item->last_execution_major++; + } + this->push_(std::move(item)); + } + } + } + + this->process_to_add(); +} +void HOT Scheduler::process_to_add() { + for (auto &it : this->to_add_) { + if (it->remove) { + continue; + } + + this->items_.push_back(std::move(it)); + std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); + } + this->to_add_.clear(); +} +void HOT Scheduler::cleanup_() { + while (!this->items_.empty()) { + auto &item = this->items_[0]; + if (!item->remove) + return; + + this->pop_raw_(); + } +} +void HOT Scheduler::pop_raw_() { + std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); + this->items_.pop_back(); +} +void HOT Scheduler::push_(std::unique_ptr<Scheduler::SchedulerItem> item) { this->to_add_.push_back(std::move(item)); } +bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) { + bool ret = false; + for (auto &it : this->items_) + if (it->component == component && it->name == name && it->type == type) { + it->remove = true; + ret = true; + } + for (auto &it : this->to_add_) + if (it->component == component && it->name == name && it->type == type) { + it->remove = true; + ret = true; + } + + return ret; +} +uint32_t Scheduler::millis_() { + const uint32_t now = millis(); + if (now < this->last_millis_) { + ESP_LOGD(TAG, "Incrementing scheduler major"); + this->millis_major_++; + } + return now; +} + +bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a, + const std::unique_ptr<SchedulerItem> &b) { + // min-heap + // return true if *a* will happen after *b* + uint32_t a_next_exec = a->next_execution(); + uint8_t a_next_exec_major = a->next_execution_major(); + uint32_t b_next_exec = b->next_execution(); + uint8_t b_next_exec_major = b->next_execution_major(); + + if (a_next_exec_major != b_next_exec_major) { + // The "major" calculation is quite complicated. + // Basically, we need to check if the major value lies in the future or + // + + // Here are some cases to think about: + // Format: a_major,b_major -> expected result (a-b, b-a) + // a=255,b=0 -> false (255, 1) + // a=0,b=1 -> false (255, 1) + // a=1,b=0 -> true (1, 255) + // a=0,b=255 -> true (1, 255) + + uint8_t diff1 = a_next_exec_major - b_next_exec_major; + uint8_t diff2 = b_next_exec_major - a_next_exec_major; + return diff1 < diff2; + } + + return a_next_exec > b_next_exec; +} + +} // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h new file mode 100644 index 0000000000..5688058a1e --- /dev/null +++ b/esphome/core/scheduler.h @@ -0,0 +1,66 @@ +#pragma once + +#include "esphome/core/component.h" +#include <vector> +#include <memory> + +namespace esphome { + +class Component; + +class Scheduler { + public: + void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function<void()> &&func); + bool cancel_timeout(Component *component, const std::string &name); + void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> &&func); + bool cancel_interval(Component *component, const std::string &name); + + optional<uint32_t> next_schedule_in(); + + void call(); + + void process_to_add(); + + protected: + struct SchedulerItem { + Component *component; + std::string name; + enum Type { TIMEOUT, INTERVAL } type; + union { + uint32_t interval; + uint32_t timeout; + }; + uint32_t last_execution; + std::function<void()> f; + bool remove; + uint8_t last_execution_major; + + inline uint32_t next_execution() { return this->last_execution + this->timeout; } + inline uint8_t next_execution_major() { + uint32_t next_exec = this->next_execution(); + uint8_t next_exec_major = this->last_execution_major; + if (next_exec < this->last_execution) + next_exec_major++; + return next_exec_major; + } + + static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b); + }; + + uint32_t millis_(); + void cleanup_(); + void pop_raw_(); + void push_(std::unique_ptr<SchedulerItem> item); + bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type); + bool empty_() { + this->cleanup_(); + return this->items_.empty(); + } + + std::vector<std::unique_ptr<SchedulerItem>> items_; + std::vector<std::unique_ptr<SchedulerItem>> to_add_; + uint32_t last_millis_{0}; + uint8_t millis_major_{0}; +}; + +} // namespace esphome diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp index 4a2985e516..ea5e347c72 100644 --- a/esphome/core/util.cpp +++ b/esphome/core/util.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/application.h" #include "esphome/core/version.h" +#include "esphome/core/log.h" #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" @@ -38,84 +39,56 @@ bool network_is_connected() { return false; } -void network_setup() { - bool ready = true; -#ifdef USE_ETHERNET - if (ethernet::global_eth_component != nullptr) { - ethernet::global_eth_component->call_setup(); - ready = false; - } -#endif - -#ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) { - wifi::global_wifi_component->call_setup(); - ready = false; - } -#endif - - while (!ready) { -#ifdef USE_ETHERNET - if (ethernet::global_eth_component != nullptr) { - ethernet::global_eth_component->call_loop(); - ready = ready || ethernet::global_eth_component->can_proceed(); - } -#endif -#ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) { - wifi::global_wifi_component->call_loop(); - ready = ready || wifi::global_wifi_component->can_proceed(); - } -#endif - - App.feed_wdt(); - } -} -void network_tick() { -#ifdef USE_ETHERNET - if (ethernet::global_eth_component != nullptr) - ethernet::global_eth_component->call_loop(); -#endif -#ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) - wifi::global_wifi_component->call_loop(); -#endif -} - -void network_setup_mdns() { - MDNS.begin(App.get_name().c_str()); -#ifdef USE_API - if (api::global_api_server != nullptr) { - MDNS.addService("esphomelib", "tcp", api::global_api_server->get_port()); - // DNS-SD (!=mDNS !) requires at least one TXT record for service discovery - let's add version - MDNS.addServiceTxt("esphomelib", "tcp", "version", ESPHOME_VERSION); - MDNS.addServiceTxt("esphomelib", "tcp", "address", network_get_address().c_str()); - } else { -#endif - // Publish "http" service if not using native API. - // This is just to have *some* mDNS service so that .local resolution works - MDNS.addService("http", "tcp", 80); - MDNS.addServiceTxt("http", "tcp", "version", ESPHOME_VERSION); -#ifdef USE_API - } -#endif -} -void network_tick_mdns() { #ifdef ARDUINO_ARCH_ESP8266 - MDNS.update(); +bool mdns_setup; #endif -} -std::string network_get_address() { +#ifdef ARDUINO_ARCH_ESP8266 +void network_setup_mdns(IPAddress address, int interface) { + // Latest arduino framework breaks mDNS for AP interface + // see https://github.com/esp8266/Arduino/issues/6114 + if (interface == 1) + return; + MDNS.begin(App.get_name().c_str(), address); + mdns_setup = true; +#endif +#ifdef ARDUINO_ARCH_ESP32 + void network_setup_mdns() { + MDNS.begin(App.get_name().c_str()); +#endif +#ifdef USE_API + if (api::global_api_server != nullptr) { + MDNS.addService("esphomelib", "tcp", api::global_api_server->get_port()); + // DNS-SD (!=mDNS !) requires at least one TXT record for service discovery - let's add version + MDNS.addServiceTxt("esphomelib", "tcp", "version", ESPHOME_VERSION); + MDNS.addServiceTxt("esphomelib", "tcp", "address", network_get_address().c_str()); + } else { +#endif + // Publish "http" service if not using native API. + // This is just to have *some* mDNS service so that .local resolution works + MDNS.addService("http", "tcp", 80); + MDNS.addServiceTxt("http", "tcp", "version", ESPHOME_VERSION); +#ifdef USE_API + } +#endif + } + void network_tick_mdns() { +#ifdef ARDUINO_ARCH_ESP8266 + if (mdns_setup) + MDNS.update(); +#endif + } + + std::string network_get_address() { #ifdef USE_ETHERNET - if (ethernet::global_eth_component != nullptr) - return ethernet::global_eth_component->get_use_address(); + if (ethernet::global_eth_component != nullptr) + return ethernet::global_eth_component->get_use_address(); #endif #ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) - return wifi::global_wifi_component->get_use_address(); + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->get_use_address(); #endif - return ""; -} + return ""; + } } // namespace esphome diff --git a/esphome/core/util.h b/esphome/core/util.h index f47eeb8439..0e121ef382 100644 --- a/esphome/core/util.h +++ b/esphome/core/util.h @@ -1,6 +1,7 @@ #pragma once #include <string> +#include "IPAddress.h" namespace esphome { @@ -10,9 +11,12 @@ bool network_is_connected(); std::string network_get_address(); /// Manually set up the network stack (outside of the App.setup() loop, for example in OTA safe mode) -void network_setup(); -void network_tick(); +#ifdef ARDUINO_ARCH_ESP8266 +void network_setup_mdns(IPAddress address, int interface); +#endif +#ifdef ARDUINO_ARCH_ESP32 void network_setup_mdns(); +#endif void network_tick_mdns(); } // namespace esphome diff --git a/esphome/core_config.py b/esphome/core_config.py index 051de896b5..f63d2e17e3 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -7,7 +7,7 @@ import esphome.config_validation as cv from esphome import automation, pins from esphome.const import ARDUINO_VERSION_ESP32_DEV, ARDUINO_VERSION_ESP8266_DEV, \ CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, \ - CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \ + CONF_COMMENT, CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \ CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \ CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_TRIGGER_ID, \ CONF_ESP8266_RESTORE_FROM_FLASH, ARDUINO_VERSION_ESP8266_2_3_0, \ @@ -45,14 +45,14 @@ def validate_board(value): validate_platform = cv.one_of('ESP32', 'ESP8266', upper=True) PLATFORMIO_ESP8266_LUT = { - '2.5.2': 'espressif8266@2.2.0', + '2.5.2': 'espressif8266@2.2.3', '2.5.1': 'espressif8266@2.1.0', '2.5.0': 'espressif8266@2.0.1', '2.4.2': 'espressif8266@1.8.0', '2.4.1': 'espressif8266@1.7.3', '2.4.0': 'espressif8266@1.6.0', '2.3.0': 'espressif8266@1.5.0', - 'RECOMMENDED': 'espressif8266@1.8.0', + 'RECOMMENDED': 'espressif8266@2.2.3', 'LATEST': 'espressif8266', 'DEV': ARDUINO_VERSION_ESP8266_DEV, } @@ -60,8 +60,10 @@ PLATFORMIO_ESP8266_LUT = { PLATFORMIO_ESP32_LUT = { '1.0.0': 'espressif32@1.4.0', '1.0.1': 'espressif32@1.6.0', - '1.0.2': 'espressif32@1.8.0', - 'RECOMMENDED': 'espressif32@1.6.0', + '1.0.2': 'espressif32@1.9.0', + '1.0.3': 'espressif32@1.10.0', + '1.0.4': 'espressif32@1.11.0', + 'RECOMMENDED': 'espressif32@1.11.0', 'LATEST': 'espressif32', 'DEV': ARDUINO_VERSION_ESP32_DEV, } @@ -113,6 +115,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Required(CONF_NAME): cv.valid_name, cv.Required(CONF_PLATFORM): cv.one_of('ESP8266', 'ESP32', upper=True), cv.Required(CONF_BOARD): validate_board, + cv.Optional(CONF_COMMENT): cv.string, cv.Optional(CONF_ARDUINO_VERSION, default='recommended'): validate_arduino_version, cv.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema({ @@ -239,7 +242,6 @@ def to_code(config): # Libraries if CORE.is_esp32: - cg.add_library('Preferences', None) cg.add_library('ESPmDNS', None) elif CORE.is_esp8266: cg.add_library('ESP8266WiFi', None) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index c1e4a87179..09b542b3cc 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -250,7 +250,7 @@ class FloatLiteral(Literal): def __str__(self): if math.isnan(self.float_): return u"NAN" - return u"{:f}f".format(self.float_) + return u"{}f".format(self.float_) # pylint: disable=bad-continuation diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 4df7b5a386..c934626da8 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -26,10 +26,10 @@ import tornado.process import tornado.web import tornado.websocket -from esphome import const +from esphome import const, util from esphome.__main__ import get_serial_ports from esphome.helpers import mkdir_p, get_bool_env, run_system_command -from esphome.py_compat import IS_PY2, decode_text +from esphome.py_compat import IS_PY2, decode_text, encode_text from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \ esphome_storage_path, ext_storage_path, trash_storage_path from esphome.util import shlex_quote @@ -46,20 +46,23 @@ class DashboardSettings(object): def __init__(self): self.config_dir = '' self.password_digest = '' + self.username = '' self.using_password = False self.on_hassio = False self.cookie_secret = None def parse_args(self, args): self.on_hassio = args.hassio + password = args.password or os.getenv('PASSWORD', '') if not self.on_hassio: - self.using_password = bool(args.password) + self.username = args.username or os.getenv('USERNAME', '') + self.using_password = bool(password) if self.using_password: if IS_PY2: - self.password_digest = hmac.new(args.password).digest() + self.password_digest = hmac.new(password).digest() else: - self.password_digest = hmac.new(args.password.encode()).digest() - self.config_dir = args.configuration + self.password_digest = hmac.new(password.encode()).digest() + self.config_dir = args.configuration[0] @property def relative_url(self): @@ -79,31 +82,20 @@ class DashboardSettings(object): def using_auth(self): return self.using_password or self.using_hassio_auth - def check_password(self, password): + def check_password(self, username, password): if not self.using_auth: return True + if username != self.username: + return False - if IS_PY2: - password = hmac.new(password).digest() - else: - password = hmac.new(password.encode()).digest() - return hmac.compare_digest(self.password_digest, password) + password_digest = hmac.new(encode_text(password)).digest() + return hmac.compare_digest(self.password_digest, password_digest) def rel_path(self, *args): return os.path.join(self.config_dir, *args) def list_yaml_files(self): - files = [] - for file in os.listdir(self.config_dir): - if not file.endswith('.yaml'): - continue - if file.startswith('.'): - continue - if file == 'secrets.yaml': - continue - files.append(file) - files.sort() - return files + return util.list_yaml_files(self.config_dir) settings = DashboardSettings() @@ -122,6 +114,7 @@ def template_args(): 'get_static_file_url': get_static_file_url, 'relative_url': settings.relative_url, 'streamer_mode': get_bool_env('ESPHOME_STREAMER_MODE'), + 'config_dir': settings.config_dir, } @@ -315,6 +308,11 @@ class EsphomeAceEditorHandler(EsphomeCommandWebSocket): return ["esphome", "--dashboard", "-q", settings.config_dir, "vscode", "--ace"] +class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): + def build_command(self, json_message): + return ["esphome", "--dashboard", settings.config_dir, "update-all"] + + class SerialPortRequestHandler(BaseHandler): @authenticated def get(self): @@ -374,14 +372,14 @@ def _list_dashboard_entries(): class DashboardEntry(object): - def __init__(self, filename): - self.filename = filename + def __init__(self, path): + self.path = path self._storage = None self._loaded_storage = False @property - def full_path(self): # type: () -> str - return os.path.join(settings.config_dir, self.filename) + def filename(self): + return os.path.basename(self.path) @property def storage(self): # type: () -> Optional[StorageJSON] @@ -402,6 +400,12 @@ class DashboardEntry(object): return self.filename[:-len('.yaml')] return self.storage.name + @property + def comment(self): + if self.storage is None: + return None + return self.storage.comment + @property def esp_platform(self): if self.storage is None: @@ -534,9 +538,12 @@ class EditRequestHandler(BaseHandler): @authenticated @bind_config def get(self, configuration=None): - # pylint: disable=no-value-for-parameter - with open(settings.rel_path(configuration), 'r') as f: - content = f.read() + filename = settings.rel_path(configuration) + content = '' + if os.path.isfile(filename): + # pylint: disable=no-value-for-parameter + with open(filename, 'r') as f: + content = f.read() self.write(content) @authenticated @@ -586,16 +593,14 @@ PING_REQUEST = threading.Event() class LoginHandler(BaseHandler): def get(self): - if settings.using_hassio_auth: - self.render_hassio_login() - return - self.write('<html><body><form action="./login" method="post">' - 'Password: <input type="password" name="password">' - '<input type="submit" value="Sign in">' - '</form></body></html>') + if is_authenticated(self): + self.redirect('/') + else: + self.render_login_page() - def render_hassio_login(self, error=None): - self.render("templates/login.html", error=error, **template_args()) + def render_login_page(self, error=None): + self.render("templates/login.html", error=error, hassio=settings.using_hassio_auth, + has_username=bool(settings.username), **template_args()) def post_hassio_login(self): import requests @@ -604,8 +609,8 @@ class LoginHandler(BaseHandler): 'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'), } data = { - 'username': str(self.get_argument('username', '')), - 'password': str(self.get_argument('password', '')) + 'username': decode_text(self.get_argument('username', '')), + 'password': decode_text(self.get_argument('password', '')) } try: req = requests.post('http://hassio/auth', headers=headers, data=data) @@ -616,20 +621,34 @@ class LoginHandler(BaseHandler): except Exception as err: # pylint: disable=broad-except _LOGGER.warning("Error during Hass.io auth request: %s", err) self.set_status(500) - self.render_hassio_login(error="Internal server error") + self.render_login_page(error="Internal server error") return self.set_status(401) - self.render_hassio_login(error="Invalid username or password") + self.render_login_page(error="Invalid username or password") + + def post_native_login(self): + username = decode_text(self.get_argument("username", '')) + password = decode_text(self.get_argument("password", '')) + if settings.check_password(username, password): + self.set_secure_cookie("authenticated", cookie_authenticated_yes) + self.redirect("/") + return + error_str = "Invalid username or password" if settings.username else "Invalid password" + self.set_status(401) + self.render_login_page(error=error_str) def post(self): if settings.using_hassio_auth: self.post_hassio_login() - return + else: + self.post_native_login() - password = str(self.get_argument("password", '')) - if settings.check_password(password): - self.set_secure_cookie("authenticated", cookie_authenticated_yes) - self.redirect("/") + +class LogoutHandler(BaseHandler): + @authenticated + def get(self): + self.clear_cookie("authenticated") + self.redirect('./login') _STATIC_FILE_HASHES = {} @@ -682,6 +701,7 @@ def make_app(debug=False): app = tornado.web.Application([ (rel + "", MainRequestHandler), (rel + "login", LoginHandler), + (rel + "logout", LogoutHandler), (rel + "logs", EsphomeLogsHandler), (rel + "upload", EsphomeUploadHandler), (rel + "compile", EsphomeCompileHandler), @@ -690,6 +710,7 @@ def make_app(debug=False): (rel + "clean", EsphomeCleanHandler), (rel + "vscode", EsphomeVscodeHandler), (rel + "ace", EsphomeAceEditorHandler), + (rel + "update-all", EsphomeUpdateAllHandler), (rel + "edit", EditRequestHandler), (rel + "download.bin", DownloadBinaryRequestHandler), (rel + "serial-ports", SerialPortRequestHandler), diff --git a/esphome/dashboard/static/esphome.css b/esphome/dashboard/static/esphome.css index 29455d9851..fddfb5cf86 100644 --- a/esphome/dashboard/static/esphome.css +++ b/esphome/dashboard/static/esphome.css @@ -47,6 +47,7 @@ i.very-large { } .log { + height: 100%; max-height: calc(100% - 56px); background-color: #1c1c1c; margin-top: 0; @@ -131,10 +132,14 @@ ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .s .select-port-container { margin-top: 8px; - margin-right: 24px; + margin-right: 10px; width: 350px; } +#dropdown-nav-trigger { + margin-right: 24px; +} + .select-port-container .select-dropdown { color: #fff; } @@ -242,3 +247,10 @@ ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .s padding: 10px 15px; margin-top: 15px; } + +.card-comment { + margin-bottom: 8px; + font-size: 14px; + color: #444; + font-style: italic; +} diff --git a/esphome/dashboard/static/esphome.js b/esphome/dashboard/static/esphome.js index 044c2d70a3..e284690ac6 100644 --- a/esphome/dashboard/static/esphome.js +++ b/esphome/dashboard/static/esphome.js @@ -333,6 +333,10 @@ class LogModalElem { this.activeSocket.close(); } + open(event) { + this._onPress(event); + } + _onPress(event) { this.activeConfig = event.target.getAttribute('data-node'); this._setupModalInstance(); @@ -570,6 +574,7 @@ const editModalElem = document.getElementById("modal-editor"); const editorElem = editModalElem.querySelector("#editor"); const editor = ace.edit(editorElem); let activeEditorConfig = null; +let activeEditorSecrets = false; let aceWs = null; let aceValidationScheduled = false; let aceValidationRunning = false; @@ -580,7 +585,6 @@ const startAceWebsocket = () => { if (raw.event === "line") { const msg = JSON.parse(raw.data); if (msg.type === "result") { - console.log(msg); const arr = []; for (const v of msg.validation_errors) { @@ -681,7 +685,7 @@ editor.commands.addCommand({ }); editor.session.on('change', debounce(() => { - aceValidationScheduled = true; + aceValidationScheduled = !activeEditorSecrets; }, 250)); setInterval(() => { @@ -704,14 +708,21 @@ editorUploadButton.addEventListener('click', saveEditor); document.querySelectorAll(".action-edit").forEach((btn) => { btn.addEventListener('click', (e) => { activeEditorConfig = e.target.getAttribute('data-node'); + activeEditorSecrets = activeEditorConfig === 'secrets.yaml'; const modalInstance = M.Modal.getInstance(editModalElem); const filenameField = editModalElem.querySelector('.filename'); editorUploadButton.setAttribute('data-node', activeEditorConfig); + if (activeEditorSecrets) { + editorUploadButton.classList.add('disabled'); + } filenameField.innerHTML = activeEditorConfig; + editor.setValue("Loading configuration yaml..."); + editor.setOption('readOnly', true); fetch(`./edit?configuration=${activeEditorConfig}`, {credentials: "same-origin"}) .then(res => res.text()).then(response => { editor.setValue(response, -1); + editor.setOption('readOnly', false); }); modalInstance.open(); @@ -742,3 +753,30 @@ jQuery.validator.addMethod("nospaces", (value, element) => { jQuery.validator.addMethod("lowercase", (value, element) => { return value === value.toLowerCase(); }, "Name must be lowercase."); + +const updateAllModal = new LogModalElem({ + name: 'update-all', + onPrepare: (modalElem, config) => { + modalElem.querySelector('.stop-logs').innerHTML = "Stop"; + downloadButton.classList.add('disabled'); + }, + onProcessExit: (modalElem, code) => { + if (code === 0) { + M.toast({html: "Program exited successfully."}); + downloadButton.classList.remove('disabled'); + } else { + M.toast({html: `Program failed with code ${data.code}`}); + } + modalElem.querySelector(".stop-logs").innerHTML = "Close"; + }, + onSocketClose: (modalElem) => { + M.toast({html: 'Terminated process.'}); + }, + dismissible: false, +}); +updateAllModal.setup(); + +const updateAllButton = document.getElementById('update-all-button'); +updateAllButton.addEventListener('click', (e) => { + updateAllModal.open(e); +}); diff --git a/esphome/dashboard/static/fonts/LICENSE b/esphome/dashboard/static/fonts/LICENSE new file mode 100644 index 0000000000..7a4a3ea242 --- /dev/null +++ b/esphome/dashboard/static/fonts/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff b/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff new file mode 100644 index 0000000000..b648a3eea2 Binary files /dev/null and b/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff differ diff --git a/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff2 b/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff2 new file mode 100644 index 0000000000..9fa2112520 Binary files /dev/null and b/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff2 differ diff --git a/esphome/dashboard/static/fonts/README.md b/esphome/dashboard/static/fonts/README.md new file mode 100644 index 0000000000..34d980de08 --- /dev/null +++ b/esphome/dashboard/static/fonts/README.md @@ -0,0 +1,12 @@ +The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts: + +```html +<link href="https://fonts.googleapis.com/icon?family=Material+Icons" + rel="stylesheet"> +``` + +Read more in our full usage guide: +http://google.github.io/material-design-icons/#icon-font-for-the-web + +Source: +https://github.com/google/material-design-icons diff --git a/esphome/dashboard/static/fonts/material-icons.css b/esphome/dashboard/static/fonts/material-icons.css new file mode 100644 index 0000000000..51f2e0a0d1 --- /dev/null +++ b/esphome/dashboard/static/fonts/material-icons.css @@ -0,0 +1,34 @@ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(MaterialIcons-Regular.woff2) format('woff2'), + url(MaterialIcons-Regular.woff) format('woff'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; /* Preferred icon size */ + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + + /* Support for all WebKit browsers. */ + -webkit-font-smoothing: antialiased; + /* Support for Safari and Chrome. */ + text-rendering: optimizeLegibility; + + /* Support for Firefox. */ + -moz-osx-font-smoothing: grayscale; + + /* Support for IE. */ + font-feature-settings: 'liga'; +} diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index 1929f42397..0aea1c8cef 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <title>ESPHome Dashboard</title> - <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> + <link rel="stylesheet" href="{{ get_static_file_url('fonts/material-icons.css') }}"> <link rel="stylesheet" href="{{ get_static_file_url('materialize.min.css') }}"> <link rel="stylesheet" href="{{ get_static_file_url('materialize-stepper.min.css') }}"> <link rel="stylesheet" href="{{ get_static_file_url('esphome.css') }}"> @@ -31,10 +31,17 @@ <nav> <div class="nav-wrapper indigo"> <a href="#" class="brand-logo left">ESPHome Dashboard</a> + <i class="material-icons dropdown-trigger right" id="dropdown-nav-trigger" data-target="dropdown-nav-actions">more_vert</i> <div class="select-port-container right" id="select-port-target"> <select></select> </div> </div> + + <ul id="dropdown-nav-actions" class="select-action dropdown-content card-dropdown-action"> + <li><a id="logout-button" href="{{ relative_url }}logout">Logout</a></li> + <li><a id="update-all-button" data-node="{{ escape(config_dir) }}">Update All</a></li> + <li><a id="secrets-button" class="action-edit" data-node="secrets.yaml">Secrets Editor</a></li> + </ul> </nav> {% if begin %} @@ -72,11 +79,16 @@ {% end %} <i class="material-icons right dropdown-trigger" data-target="dropdown-{{ i }}">more_vert</i> </span> + {% if entry.comment %} + <div class="card-comment"> + {{ escape(entry.comment) }} + </div> + {% end %} <p> <span class="status-indicator unknown" data-node="{{ entry.filename }}"> <span class="status-indicator-icon"></span> <span class="status-indicator-text"></span></span>. - Full path: <code class="inlinecode">{{ escape(entry.full_path) }}</code> + Full path: <code class="inlinecode">{{ escape(entry.path) }}</code> </p> {% if entry.update_available %} <p class="update-available" data-node="{{ entry.filename }}"> @@ -268,52 +280,70 @@ </optgroup> <optgroup label="Other ESP32s"> <option value="lolin32">Wemos Lolin 32</option> - <option value="esp32dev">Espressif ESP32 Dev Module</option> <option value="m5stack-fire">M5Stack FIRE</option> <option value="wemosbat">Wemos WiFi & Bluetooth Battery</option> - <option value="node32s">Aiyarafun Node32s</option> - <option value="espea32">April Brother ESPea32</option> - <option value="firebeetle32">DFRobot FireBeetle-ESP32</option> - <option value="esp32doit-devkit-v1">Doit ESP32 Devkit v1</option> - <option value="pocket_32">Dongsen Tech Pocket 32</option> - <option value="espectro32">DycodeX ESPectro32</option> - <option value="esp32vn-iot-uno">ESP32vn IoT Uno</option> + <option value="node32s">Node32s</option> + + <option value="alksesp32">ALKS ESP32</option> + <option value="bpi-bit">BPI-Bit</option> + <option value="d-duino-32">D-duino-32</option> + <option value="esp32-devkitlipo">OLIMEX ESP32-DevKit-LiPo</option> + <option value="esp32-evb">OLIMEX ESP32-EVB</option> + <option value="esp32-gateway">OLIMEX ESP32-GATEWAY</option> + <option value="esp32-poe-iso">OLIMEX ESP32-PoE-ISO</option> + <option value="esp32-poe">OLIMEX ESP32-PoE</option> + <option value="esp32-pro">OLIMEX ESP32-PRO</option> <option value="esp320">Electronic SweetPeas ESP320</option> - <option value="pico32">Espressif ESP32 Pico Kit</option> - <option value="odroid_esp32">Hardkernel Odroid GO</option> - <option value="heltec_wifi_kit_32">Heltec WIFI Kit 32</option> - <option value="heltec_wifi_lora_32">Heltec WIFI LoRa 32</option> + <option value="esp32cam">AI Thinker ESP32-CAM</option> + <option value="esp32dev">Espressif ESP32 Dev Module</option> + <option value="esp32doit-devkit-v1">DOIT ESP32 DEVKIT V1</option> + <option value="esp32thing">SparkFun ESP32 Thing</option> + <option value="esp32vn-iot-uno">ESP32vn IoT Uno</option> + <option value="espea32">April Brother ESPea32</option> + <option value="espectro32">ESPectro32</option> + <option value="espino32">ESPino32</option> + <option value="firebeetle32">FireBeetle-ESP32</option> + <option value="fm-devkit">ESP32 FM DevKit</option> + <option value="frogboard">Frog Board ESP32</option> + <option value="heltec_wifi_kit_32">Heltec WiFi Kit 32</option> + <option value="heltec_wifi_lora_32">Heltec WiFi LoRa 32</option> + <option value="heltec_wifi_lora_32_V2">Heltec WiFi LoRa 32 (V2)</option> + <option value="heltec_wireless_stick">Heltec Wireless Stick</option> <option value="hornbill32dev">Hornbill ESP32 Dev</option> <option value="hornbill32minima">Hornbill ESP32 Minima</option> <option value="intorobot">IntoRobot Fig</option> - <option value="mhetesp32devkit">MH-ET Live ESP32 Devkit</option> - <option value="mhetesp32minikit">MH-ET Live ESP32 Minikit</option> - <option value="nano32">MakerAsia Nano32</option> - <option value="microduino-core-esp32">Microduino Core ESP32</option> - <option value="quantum">Noduino Quantum</option> - <option value="esp32-evb">Olimex ESP32-EVB</option> - <option value="esp32-gateway">Olimex ESP32-GATEWAY</option> - <option value="esp32-pro">Olimex ESP32-PRO</option> - <option value="onehorse32dev">Onehorse ESP32 Dev Module</option> - <option value="alksesp32">RoboticsBrno ALKS ESP32</option> - <option value="esp32thing">Sparkfun ESP32 Thing</option> - <option value="ttgo-lora32-v1">TTGO LoRa32-OLED v1</option> - <option value="espino32">ThaiEasyElec ESPino32</option> - <option value="widora-air">Widora AIR</option> - <option value="xinabox_cw02">XinaBox CW02</option> + <option value="iotaap_magnolia">IoTaaP Magnolia</option> <option value="iotbusio">oddWires IoT-Bus Io</option> - <option value="iotbusproteus">oddWires Proteus IoT-Bus</option> - <option value="nina_w10">u-blox NINA-W10 series</option> - <option value="bpi-bit">BananaPi-Bit</option> - <option value="d-duino-32">DSTIKE D-duino-32</option> - <option value="fm-devkit">ESP32 FM DevKit</option> - <option value="esp32-poe">OLIMEX ESP32-PoE</option> - <option value="oroca_edubot">OROCA EduBot</option> + <option value="iotbusproteus">oddWires IoT-Bus Proteus</option> <option value="lopy">Pycom LoPy</option> <option value="lopy4">Pycom LoPy4</option> - <option value="wesp32">Silicognition wESP32</option> + <option value="m5stack-grey">M5Stack GREY ESP32</option> + <option value="m5stick-c">M5Stick-C</option> + <option value="magicbit">MagicBit</option> + <option value="mhetesp32devkit">MH ET LIVE ESP32DevKIT</option> + <option value="mhetesp32minikit">MH ET LIVE ESP32MiniKit</option> + <option value="microduino-core-esp32">Microduino Core ESP32</option> + <option value="nano32">MakerAsia Nano32</option> + <option value="nina_w10">u-blox NINA-W10 series</option> + <option value="odroid_esp32">ODROID-GO</option> + <option value="onehorse32dev">Onehorse ESP32 Dev Module</option> + <option value="oroca_edubot">OROCA EduBot</option> + <option value="pico32">ESP32 Pico Kit</option> + <option value="pocket_32">Dongsen Tech Pocket 32</option> + <option value="pycom_gpy">Pycom GPy</option> + <option value="quantum">Noduino Quantum</option> + <option value="sparkfun_lora_gateway_1-channel">SparkFun LoRa Gateway 1-Channel</option> + <option value="tinypico">TinyPICO</option> + <option value="ttgo-lora32-v1">TTGO LoRa32-OLED V1</option> <option value="ttgo-t-beam">TTGO T-Beam</option> + <option value="ttgo-t-watch">TTGO T-Watch</option> + <option value="ttgo-t1">TTGO T1</option> <option value="turta_iot_node">Turta IoT Node</option> + <option value="vintlabs-devkit-v1">VintLabs ESP32 Devkit</option> + <option value="wemos_d1_mini32">WeMos D1 MINI ESP32</option> + <option value="wesp32">Silicognition wESP32</option> + <option value="widora-air">Widora AIR</option> + <option value="xinabox_cw02">XinaBox CW02</option> </optgroup> </select> </div> @@ -445,6 +475,16 @@ </div> </div> +<div id="modal-update-all" class="modal modal-fixed-footer"> + <div class="modal-content"> + <h4>Update All</h4> + <pre class="log"></pre> + </div> + <div class="modal-footer"> + <a class="modal-close waves-effect waves-green btn-flat stop-logs">Stop</a> + </div> +</div> + <a class="btn-floating btn-large ribbon-fab waves-effect waves-light pink accent-2" id="setup-wizard-start"> <i class="material-icons">add</i> </a> diff --git a/esphome/dashboard/templates/login.html b/esphome/dashboard/templates/login.html index cfe682e776..414617c17f 100644 --- a/esphome/dashboard/templates/login.html +++ b/esphome/dashboard/templates/login.html @@ -2,10 +2,10 @@ <html lang="en"> <head> <meta charset="UTF-8"> - <title>ESPHome Dashboard</title> - <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> + <title>ESPHome Dashboard</title> + <link rel="stylesheet" href="{{ get_static_file_url('fonts/material-icons.css') }}"> <link rel="stylesheet" href="{{ get_static_file_url('materialize.min.css') }}"> - <link href="{{ get_static_file_url('esphome.css') }}" rel="stylesheet"> + <link rel="stylesheet" href="{{ get_static_file_url('esphome.css') }}"> <link rel="shortcut icon" href="{{ get_static_file_url('favicon.ico') }}"> <script src="{{ get_static_file_url('materialize.min.js') }}"></script> @@ -31,19 +31,23 @@ <form action="./login" method="post"> <div class="card-content"> <span class="card-title">Enter credentials</span> - <p> - Please login using your Home Assistant credentials. - </p> + {% if hassio %} + <p> + Please login using your Home Assistant credentials. + </p> + {% end %} {% if error is not None %} <p class="error"> {{ escape(error) }} </p> {% end %} <div class="row"> - <div class="input-field col s12"> - <label for="username">Username</label> - <input type="text" class="validate" name="username" id="username" /> - </div> + {% if has_username or hassio %} + <div class="input-field col s12"> + <label for="username">Username</label> + <input type="text" class="validate" name="username" id="username" /> + </div> + {% end %} <div class="input-field col s12"> <label for="password">Password</label> <input type="password" class="validate" name="password" id="password" /> diff --git a/esphome/espota2.py b/esphome/espota2.py index 786f49dbdf..40417b9ab2 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -127,7 +127,8 @@ def check_error(data, expect): "correct 'board' option (esp01_1m always works) and then flash over USB.") if dat == RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG: raise OTAError("Error: ESP does not have the requested flash size (wrong board). Please " - "choose the correct 'board' option (esp01_1m always works) and try again.") + "choose the correct 'board' option (esp01_1m always works) and try " + "uploading again.") if dat == RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE: raise OTAError("Error: ESP does not have enough space to store OTA file. Please try " "flashing a minimal firmware (remove everything except ota)") @@ -299,3 +300,4 @@ def run_ota(remote_host, remote_port, password, filename): return run_ota_impl_(remote_host, remote_port, password, filename) except OTAError as err: _LOGGER.error(err) + return 1 diff --git a/esphome/helpers.py b/esphome/helpers.py index 30a06d842f..6fd1fa2ad7 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,10 +1,11 @@ from __future__ import print_function import codecs + import logging import os -from esphome.py_compat import char_to_byte, text_type +from esphome.py_compat import char_to_byte, text_type, IS_PY2, encode_text _LOGGER = logging.getLogger(__name__) @@ -79,15 +80,15 @@ def run_system_command(*args): def mkdir_p(path): - import errno - try: os.makedirs(path) - except OSError as exc: - if exc.errno == errno.EEXIST and os.path.isdir(path): + except OSError as err: + import errno + if err.errno == errno.EEXIST and os.path.isdir(path): pass else: - raise + from esphome.core import EsphomeError + raise EsphomeError(u"Error creating directories {}: {}".format(path, err)) def is_ip_address(host): @@ -151,17 +152,6 @@ def is_hassio(): return get_bool_env('ESPHOME_IS_HASSIO') -def copy_file_if_changed(src, dst): - src_text = read_file(src) - if os.path.isfile(dst): - dst_text = read_file(dst) - else: - dst_text = None - if src_text == dst_text: - return - write_file(dst, src_text) - - def walk_files(path): for root, _, files in os.walk(path): for name in files: @@ -172,28 +162,99 @@ def read_file(path): try: with codecs.open(path, 'r', encoding='utf-8') as f_handle: return f_handle.read() - except OSError: + except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(u"Could not read file at {}".format(path)) + raise EsphomeError(u"Error reading file {}: {}".format(path, err)) + except UnicodeDecodeError as err: + from esphome.core import EsphomeError + raise EsphomeError(u"Error reading file {}: {}".format(path, err)) + + +def _write_file(path, text): + import tempfile + directory = os.path.dirname(path) + mkdir_p(directory) + + tmp_path = None + data = encode_text(text) + try: + with tempfile.NamedTemporaryFile(mode="wb", dir=directory, delete=False) as f_handle: + tmp_path = f_handle.name + f_handle.write(data) + # Newer tempfile implementations create the file with mode 0o600 + os.chmod(tmp_path, 0o644) + if IS_PY2: + if os.path.exists(path): + os.remove(path) + os.rename(tmp_path, path) + else: + # If destination exists, will be overwritten + os.replace(tmp_path, path) + finally: + if tmp_path is not None and os.path.exists(tmp_path): + try: + os.remove(tmp_path) + except OSError as err: + _LOGGER.error("Write file cleanup failed: %s", err) def write_file(path, text): try: - mkdir_p(os.path.dirname(path)) - with codecs.open(path, 'w+', encoding='utf-8') as f_handle: - f_handle.write(text) + _write_file(path, text) except OSError: from esphome.core import EsphomeError raise EsphomeError(u"Could not write file at {}".format(path)) -def write_file_if_changed(text, dst): +def write_file_if_changed(path, text): src_content = None - if os.path.isfile(dst): - src_content = read_file(dst) + if os.path.isfile(path): + src_content = read_file(path) if src_content != text: - write_file(dst, text) + write_file(path, text) + + +def copy_file_if_changed(src, dst): + import shutil + if file_compare(src, dst): + return + mkdir_p(os.path.dirname(dst)) + try: + shutil.copy(src, dst) + except OSError as err: + from esphome.core import EsphomeError + raise EsphomeError(u"Error copying file {} to {}: {}".format(src, dst, err)) def list_starts_with(list_, sub): return len(sub) <= len(list_) and all(list_[i] == x for i, x in enumerate(sub)) + + +def file_compare(path1, path2): + """Return True if the files path1 and path2 have the same contents.""" + import stat + + try: + stat1, stat2 = os.stat(path1), os.stat(path2) + except OSError: + # File doesn't exist or another error -> not equal + return False + + if stat.S_IFMT(stat1.st_mode) != stat.S_IFREG or stat.S_IFMT(stat2.st_mode) != stat.S_IFREG: + # At least one of them is not a regular file (or does not exist) + return False + if stat1.st_size != stat2.st_size: + # Different sizes + return False + + bufsize = 8*1024 + # Read files in blocks until a mismatch is found + with open(path1, 'rb') as fh1, open(path2, 'rb') as fh2: + while True: + blob1, blob2 = fh1.read(bufsize), fh2.read(bufsize) + if blob1 != blob2: + # Different content + return False + if not blob1: + # Reached end + return True diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 0e00459944..e89a6d9578 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -15,6 +15,7 @@ from esphome.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOME, \ CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME from esphome.core import CORE, EsphomeError from esphome.helpers import color +from esphome.py_compat import decode_text from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -22,6 +23,7 @@ _LOGGER = logging.getLogger(__name__) def initialize(config, subscriptions, on_message, username, password, client_id): def on_connect(client, userdata, flags, return_code): + _LOGGER.info("Connected to MQTT broker!") for topic in subscriptions: client.subscribe(topic) @@ -94,7 +96,8 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): def on_message(client, userdata, msg): time_ = datetime.now().time().strftime(u'[%H:%M:%S]') - message = time_ + msg.payload + payload = decode_text(msg.payload) + message = time_ + payload safe_print(message) return initialize(config, [topic], on_message, username, password, client_id) diff --git a/esphome/pins.py b/esphome/pins.py index e96dfe2e39..42c8548da4 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -36,6 +36,7 @@ ESP8266_BOARD_PINS = { 'gen4iod': {}, 'heltec_wifi_kit_8': 'd1_mini', 'huzzah': {'LED': 0}, + 'inventone': {}, 'modwifi': {}, 'nodemcu': {'D0': 16, 'D1': 5, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 15, 'D9': 3, 'D10': 1, 'LED': 16}, @@ -84,6 +85,7 @@ ESP8266_FLASH_SIZES = { 'gen4iod': FLASH_SIZE_512_KB, 'heltec_wifi_kit_8': FLASH_SIZE_4_MB, 'huzzah': FLASH_SIZE_4_MB, + 'inventone': FLASH_SIZE_4_MB, 'modwifi': FLASH_SIZE_2_MB, 'nodemcu': FLASH_SIZE_4_MB, 'nodemcuv2': FLASH_SIZE_4_MB, @@ -118,113 +120,137 @@ ESP32_BASE_PINS = { } ESP32_BOARD_PINS = { - 'alksesp32': {'D0': 40, 'D1': 41, 'D2': 15, 'D3': 2, 'D4': 0, 'D5': 4, 'D6': 16, 'D7': 17, - 'D8': 5, 'D9': 18, 'D10': 19, 'D11': 21, 'D12': 22, 'D13': 23, 'A0': 32, 'A1': 33, - 'A2': 25, 'A3': 26, 'A4': 27, 'A5': 14, 'A6': 12, 'A7': 15, 'L_R': 22, 'L_G': 17, - 'L_Y': 23, 'L_B': 5, 'L_RGB_R': 4, 'L_RGB_G': 21, 'L_RGB_B': 16, 'SW1': 15, - 'SW2': 2, 'SW3': 0, 'POT1': 32, 'POT2': 33, 'PIEZO1': 19, 'PIEZO2': 18, - 'PHOTO': 25, 'DHT_PIN': 26, 'S1': 4, 'S2': 16, 'S3': 18, 'S4': 19, 'S5': 21, - 'SDA': 27, 'SCL': 14, 'SS': 19, 'MOSI': 21, 'MISO': 22, 'SCK': 23}, - 'bpi-bit': {'BUZZER': 25, 'BUTTON_A': 35, 'BUTTON_B': 27, 'RGB_LED': 4, 'LIGHT_SENSOR1': 36, - 'LIGHT_SENSOR2': 39, 'TEMPERATURE_SENSOR': 34, 'MPU9250_INT': 0, 'P0': 25, 'P1': 32, - 'P2': 33, 'P3': 13, 'P4': 15, 'P5': 35, 'P6': 12, 'P7': 14, 'P8': 16, 'P9': 17, - 'P10': 26, 'P11': 27, 'P12': 2, 'P13': 18, 'P14': 19, 'P15': 23, 'P16': 5, - 'P19': 22, 'P20': 21, 'DAC1': 26}, - 'd-duino-32': {'SDA': 5, 'SCL': 4, 'SS ': 15, 'MOSI ': 13, 'MISO ': 12, 'SCK ': 14, 'D1': 5, - 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 15, 'D9': 3, - 'D10': 1}, + 'alksesp32': {'A0': 32, 'A1': 33, 'A2': 25, 'A3': 26, 'A4': 27, 'A5': 14, 'A6': 12, 'A7': 15, + 'D0': 40, 'D1': 41, 'D10': 19, 'D11': 21, 'D12': 22, 'D13': 23, 'D2': 15, + 'D3': 2, 'D4': 0, 'D5': 4, 'D6': 16, 'D7': 17, 'D8': 5, 'D9': 18, 'DHT_PIN': 26, + 'LED': 23, 'L_B': 5, 'L_G': 17, 'L_R': 22, 'L_RGB_B': 16, 'L_RGB_G': 21, + 'L_RGB_R': 4, 'L_Y': 23, 'MISO': 22, 'MOSI': 21, 'PHOTO': 25, 'PIEZO1': 19, + 'PIEZO2': 18, 'POT1': 32, 'POT2': 33, 'S1': 4, 'S2': 16, 'S3': 18, 'S4': 19, + 'S5': 21, 'SCK': 23, 'SCL': 14, 'SDA': 27, 'SS': 19, 'SW1': 15, 'SW2': 2, + 'SW3': 0}, + 'bpi-bit': {'BUTTON_A': 35, 'BUTTON_B': 27, 'BUZZER': 25, 'LIGHT_SENSOR1': 36, + 'LIGHT_SENSOR2': 39, 'MPU9250_INT': 0, 'P0': 25, 'P1': 32, 'P10': 26, 'P11': 27, + 'P12': 2, 'P13': 18, 'P14': 19, 'P15': 23, 'P16': 5, 'P19': 22, 'P2': 33, + 'P20': 21, 'P3': 13, 'P4': 15, 'P5': 35, 'P6': 12, 'P7': 14, 'P8': 16, 'P9': 17, + 'RGB_LED': 4, 'TEMPERATURE_SENSOR': 34}, + 'd-duino-32': {'D1': 5, 'D10': 1, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, + 'D8': 15, 'D9': 3, 'MISO': 12, 'MOSI': 13, 'SCK': 14, 'SCL': 4, 'SDA': 5, + 'SS': 15}, 'esp-wrover-kit': {}, - 'esp32-evb': {'BUTTON': 34, 'SDA': 13, 'SCL': 16, 'SS': 17, 'MOSI': 2, 'MISO': 15, 'SCK': 14}, - 'esp32-gateway': {'LED': 33, 'BUTTON': 34, 'SCL': 16, 'SDA': 17}, - 'esp32-poe': {'BUTTON': 34, 'SDA': 13, 'SCL': 16, 'MOSI': 2, 'MISO': 15, 'SCK': 14}, - 'esp320': {'LED': 5, 'SDA': 2, 'SCL': 14, 'SS': 15, 'MOSI': 13, 'MISO': 12, 'SCK': 14}, + 'esp32-devkitlipo': {}, + 'esp32-evb': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13, 'SS': 17}, + 'esp32-gateway': {'BUTTON': 34, 'LED': 33, 'SCL': 16, 'SDA': 32}, + 'esp32-poe-iso': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13}, + 'esp32-poe': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13}, + 'esp32-pro': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13, 'SS': 17}, + 'esp320': {'LED': 5, 'MISO': 12, 'MOSI': 13, 'SCK': 14, 'SCL': 14, 'SDA': 2, 'SS': 15}, + 'esp32cam': {}, 'esp32dev': {}, 'esp32doit-devkit-v1': {'LED': 2}, - 'esp32thing': {'LED': 5, 'BUTTON': 0, 'SS': 2}, + 'esp32thing': {'BUTTON': 0, 'LED': 5, 'SS': 2}, 'esp32vn-iot-uno': {}, - 'espea32': {'LED': 5, 'BUTTON': 0}, + 'espea32': {'BUTTON': 0, 'LED': 5}, 'espectro32': {'LED': 15, 'SD_SS': 33}, - 'espino32': {'LED': 16, 'BUTTON': 0}, - 'featheresp32': {'LED': 13, 'TX': 17, 'RX': 16, 'SDA': 23, 'SS': 2, 'MOSI': 18, 'SCK': 5, - 'A0': 26, 'A1': 25, 'A2': 34, 'A4': 36, 'A5': 4, 'A6': 14, 'A7': 32, 'A8': 15, - 'A9': 33, 'A10': 27, 'A11': 12, 'A12': 13, 'A13': 35}, + 'espino32': {'BUTTON': 0, 'LED': 16}, + 'featheresp32': {'A0': 26, 'A1': 25, 'A10': 27, 'A11': 12, 'A12': 13, 'A13': 35, 'A2': 34, + 'A4': 36, 'A5': 4, 'A6': 14, 'A7': 32, 'A8': 15, 'A9': 33, 'Ax': 2, + 'LED': 13, 'MOSI': 18, 'RX': 16, 'SCK': 5, 'SDA': 23, 'SS': 33, 'TX': 17}, 'firebeetle32': {'LED': 2}, - 'fm-devkit': {'LED': 5, 'SW1': 4, 'SW2': 18, 'SW3': 19, 'SW4': 21, 'I2S_MCLK': 2, - 'I2S_LRCLK': 25, 'I2S_SCLK': 26, 'I2S_DOUT': 22, 'D0': 34, 'D1': 35, 'D2': 32, - 'D3': 33, 'D4': 27, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 15, 'D9': 23, 'D10': 0, - 'SDA': 16, 'SCL': 17}, - 'heltec_wifi_kit_32': {'LED': 25, 'BUTTON': 0, 'A1': 37, 'A2': 38}, - 'heltec_wifi_lora_32': {'LED': 25, 'BUTTON': 0, 'SDA': 4, 'SCL': 15, 'SS': 18, 'MOSI': 27, - 'SCK': 5, 'A1': 37, 'A2': 38, 'OLED_SCL': 15, 'OLED_SDA': 4, - 'OLED_RST': 16, 'LORA_SCK': 5, 'LORA_MOSI': 27, 'LORA_MISO': 19, - 'LORA_CS': 18, 'LORA_RST': 14, 'LORA_IRQ': 26}, - 'hornbill32dev': {'LED': 13, 'BUTTON': 0}, + 'fm-devkit': {'D0': 34, 'D1': 35, 'D10': 0, 'D2': 32, 'D3': 33, 'D4': 27, 'D5': 14, 'D6': 12, + 'D7': 13, 'D8': 15, 'D9': 23, 'I2S_DOUT': 22, 'I2S_LRCLK': 25, 'I2S_MCLK': 2, + 'I2S_SCLK': 26, 'LED': 5, 'SCL': 17, 'SDA': 16, 'SW1': 4, 'SW2': 18, 'SW3': 19, + 'SW4': 21}, + 'frogboard': {}, + 'heltec_wifi_kit_32': {'A1': 37, 'A2': 38, 'BUTTON': 0, 'LED': 25, 'RST_OLED': 16, + 'SCL_OLED': 15, 'SDA_OLED': 4, 'Vext': 21}, + 'heltec_wifi_lora_32': {'BUTTON': 0, 'DIO0': 26, 'DIO1': 33, 'DIO2': 32, 'LED': 25, + 'MOSI': 27, 'RST_LoRa': 14, 'RST_OLED': 16, 'SCK': 5, 'SCL_OLED': 15, + 'SDA_OLED': 4, 'SS': 18, 'Vext': 21}, + 'heltec_wifi_lora_32_V2': {'BUTTON': 0, 'DIO0': 26, 'DIO1': 35, 'DIO2': 34, 'LED': 25, + 'MOSI': 27, 'RST_LoRa': 14, 'RST_OLED': 16, 'SCK': 5, + 'SCL_OLED': 15, 'SDA_OLED': 4, 'SS': 18, 'Vext': 21}, + 'heltec_wireless_stick': {'BUTTON': 0, 'DIO0': 26, 'DIO1': 35, 'DIO2': 34, 'LED': 25, + 'MOSI': 27, 'RST_LoRa': 14, 'RST_OLED': 16, 'SCK': 5, + 'SCL_OLED': 15, 'SDA_OLED': 4, 'SS': 18, 'Vext': 21}, + 'hornbill32dev': {'BUTTON': 0, 'LED': 13}, 'hornbill32minima': {'SS': 2}, - 'intorobot': {'LED': 4, 'LED_RED': 27, 'LED_GREEN': 21, 'LED_BLUE': 22, - 'BUTTON': 0, 'SDA': 23, 'SCL': 19, 'MOSI': 16, 'MISO': 17, 'A1': 39, 'A2': 35, - 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, 'A9': 2, 'D0': 19, - 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5, 'D6': 4, 'T0': 19, 'T1': 23, - 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4}, + 'intorobot': {'A1': 39, 'A2': 35, 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, + 'A9': 2, 'BUTTON': 0, 'D0': 19, 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5, + 'D6': 4, 'LED': 4, 'MISO': 17, 'MOSI': 16, 'RGB_B_BUILTIN': 22, + 'RGB_G_BUILTIN': 21, 'RGB_R_BUILTIN': 27, 'SCL': 19, 'SDA': 23, 'T0': 19, + 'T1': 23, 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4}, + 'iotaap_magnolia': {}, + 'iotbusio': {}, + 'iotbusproteus': {}, 'lolin32': {'LED': 5}, - 'lolin_d32': {'LED': 5, 'VBAT': 35}, - 'lolin_d32_pro': {'LED': 5, 'VBAT': 35, 'TF_CS': 4, 'TS_CS': 12, 'TFT_CS': 14, 'TFT_LED': 32, - 'TFT_RST': 33, 'TFT_DC': 27}, - 'lopy': {'LORA_SCK': 5, 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 17, 'LORA_RST': 18, - 'LORA_IRQ': 23, 'LED': 0, 'ANT_SELECT': 16, 'SDA': 12, 'SCL': 13, 'SS': 17, - 'MOSI': 22, 'MISO': 37, 'SCK': 13, 'A1': 37, 'A2': 38}, - 'lopy4': {'LORA_SCK': 5, 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 18, 'LORA_IRQ': 23, - 'LED': 0, 'ANT_SELECT': 21, 'SDA': 12, 'SCL': 13, 'SS': 18, 'MOSI': 22, 'MISO': 37, - 'SCK': 13, 'A1': 37, 'A2': 38}, - 'm5stack-core-esp32': {'TXD2': 17, 'RXD2': 16, 'G23': 23, 'G19': 19, 'G18': 18, 'G3': 3, - 'G16': 16, 'G21': 21, 'G2': 2, 'G12': 12, 'G15': 15, 'G35': 35, - 'G36': 36, 'G25': 25, 'G26': 26, 'G1': 1, 'G17': 17, 'G22': 22, 'G5': 5, - 'G13': 13, 'G0': 0, 'G34': 34, 'ADC1': 35, 'ADC2': 36}, - 'm5stack-fire': {'G23': 23, 'G19': 19, 'G18': 18, 'G3': 3, 'G16': 16, 'G21': 21, 'G2': 2, - 'G12': 12, 'G15': 15, 'G35': 35, 'G36': 36, 'G25': 25, 'G26': 26, 'G1': 1, - 'G17': 17, 'G22': 22, 'G5': 5, 'G13': 13, 'G0': 0, 'G34': 34, 'ADC1': 35, - 'ADC2': 36}, + 'lolin_d32': {'LED': 5, '_VBAT': 35}, + 'lolin_d32_pro': {'LED': 5, '_VBAT': 35}, + 'lopy': {'A1': 37, 'A2': 38, 'LED': 0, 'MISO': 37, 'MOSI': 22, 'SCK': 13, 'SCL': 13, + 'SDA': 12, 'SS': 17}, + 'lopy4': {'A1': 37, 'A2': 38, 'LED': 0, 'MISO': 37, 'MOSI': 22, 'SCK': 13, 'SCL': 13, + 'SDA': 12, 'SS': 18}, + 'm5stack-core-esp32': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G1': 1, 'G12': 12, 'G13': 13, + 'G15': 15, 'G16': 16, 'G17': 17, 'G18': 18, 'G19': 19, 'G2': 2, + 'G21': 21, 'G22': 22, 'G23': 23, 'G25': 25, 'G26': 26, 'G3': 3, + 'G34': 34, 'G35': 35, 'G36': 36, 'G5': 5, 'RXD2': 16, 'TXD2': 17}, + 'm5stack-fire': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G1': 1, 'G12': 12, 'G13': 13, 'G15': 15, + 'G16': 16, 'G17': 17, 'G18': 18, 'G19': 19, 'G2': 2, 'G21': 21, 'G22': 22, + 'G23': 23, 'G25': 25, 'G26': 26, 'G3': 3, 'G34': 34, 'G35': 35, 'G36': 36, + 'G5': 5}, + 'm5stack-grey': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G1': 1, 'G12': 12, 'G13': 13, 'G15': 15, + 'G16': 16, 'G17': 17, 'G18': 18, 'G19': 19, 'G2': 2, 'G21': 21, 'G22': 22, + 'G23': 23, 'G25': 25, 'G26': 26, 'G3': 3, 'G34': 34, 'G35': 35, 'G36': 36, + 'G5': 5, 'RXD2': 16, 'TXD2': 17}, + 'm5stick-c': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G10': 10, 'G26': 26, 'G32': 32, 'G33': 33, + 'G36': 36, 'G37': 37, 'G39': 39, 'G9': 9, 'MISO': 36, 'MOSI': 15, 'SCK': 13, + 'SCL': 33, 'SDA': 32}, + 'magicbit': {'BLUE_LED': 17, 'BUZZER': 25, 'GREEN_LED': 16, 'LDR': 36, 'LED': 16, + 'LEFT_BUTTON': 35, 'MOTOR1A': 27, 'MOTOR1B': 18, 'MOTOR2A': 16, 'MOTOR2B': 17, + 'POT': 39, 'RED_LED': 27, 'RIGHT_PUTTON': 34, 'YELLOW_LED': 18}, 'mhetesp32devkit': {'LED': 2}, 'mhetesp32minikit': {'LED': 2}, - 'microduino-core-esp32': {'SDA': 22, 'SCL': 21, 'SDA1': 12, 'SCL1': 13, 'A0': 12, 'A1': 13, - 'A2': 15, 'A3': 4, 'A6': 38, 'A7': 37, 'A8': 32, 'A9': 33, 'A10': 25, - 'A11': 26, 'A12': 27, 'A13': 14, 'D0': 3, 'D1': 1, 'D2': 16, 'D3': 17, - 'D4': 32, 'D5': 33, 'D6': 25, 'D7': 26, 'D8': 27, 'D9': 14, 'D10': 5, - 'D11': 23, 'D12': 19, 'D13': 18, 'D14': 12, 'D15': 13, 'D16': 15, - 'D17': 4, 'D18': 22, 'D19': 21, 'D20': 38, 'D21': 37}, - 'nano32': {'LED': 16, 'BUTTON': 0}, - 'nina_w10': {'LED_GREEN': 33, 'LED_RED': 23, 'LED_BLUE': 21, 'SW1': 33, 'SW2': 27, 'SDA': 12, - 'SCL': 13, 'D0': 3, 'D1': 1, 'D2': 26, 'D3': 25, 'D4': 35, 'D5': 27, 'D6': 22, - 'D7': 0, 'D8': 15, 'D9': 14, 'D10': 5, 'D11': 19, 'D12': 23, 'D13': 18, 'D14': 13, - 'D15': 12, 'D16': 32, 'D17': 33, 'D18': 21, 'D19': 34, 'D20': 36, 'D21': 39}, + 'microduino-core-esp32': {'A0': 12, 'A1': 13, 'A10': 25, 'A11': 26, 'A12': 27, 'A13': 14, + 'A2': 15, 'A3': 4, 'A6': 38, 'A7': 37, 'A8': 32, 'A9': 33, 'D0': 3, + 'D1': 1, 'D10': 5, 'D11': 23, 'D12': 19, 'D13': 18, 'D14': 12, + 'D15': 13, 'D16': 15, 'D17': 4, 'D18': 22, 'D19': 21, 'D2': 16, + 'D20': 38, 'D21': 37, 'D3': 17, 'D4': 32, 'D5': 33, 'D6': 25, + 'D7': 26, 'D8': 27, 'D9': 14, 'SCL': 21, 'SCL1': 13, 'SDA': 22, + 'SDA1': 12}, + 'nano32': {'BUTTON': 0, 'LED': 16}, + 'nina_w10': {'D0': 3, 'D1': 1, 'D10': 5, 'D11': 19, 'D12': 23, 'D13': 18, 'D14': 13, + 'D15': 12, 'D16': 32, 'D17': 33, 'D18': 21, 'D19': 34, 'D2': 26, 'D20': 36, + 'D21': 39, 'D3': 25, 'D4': 35, 'D5': 27, 'D6': 22, 'D7': 0, 'D8': 15, 'D9': 14, + 'LED_BLUE': 21, 'LED_GREEN': 33, 'LED_RED': 23, 'SCL': 13, 'SDA': 12, 'SW1': 33, + 'SW2': 27}, 'node32s': {}, - 'nodemcu-32s': {'LED': 2, 'BUTTON': 0}, - 'odroid_esp32': {'LED': 2, 'SDA': 15, 'SCL': 4, 'SS': 22, 'ADC1': 35, 'ADC2': 36}, - 'onehorse32dev': {'LED': 5, 'BUTTON': 0, 'A1': 37, 'A2': 38}, - 'oroca_edubot': {'LED': 13, 'TX': 17, 'RX': 16, 'SDA': 23, 'SS': 2, 'MOSI': 18, 'SCK': 5, - 'A0': 34, 'A1': 39, 'A2': 36, 'A3': 33, 'D0': 4, 'D1': 16, 'D2': 17, 'D3': 22, - 'D4': 23, 'D5': 5, 'D6': 18, 'D7': 19, 'D8': 33, 'VBAT': 35}, + 'nodemcu-32s': {'BUTTON': 0, 'LED': 2}, + 'odroid_esp32': {'ADC1': 35, 'ADC2': 36, 'LED': 2, 'SCL': 4, 'SDA': 15, 'SS': 22}, + 'onehorse32dev': {'A1': 37, 'A2': 38, 'BUTTON': 0, 'LED': 5}, + 'oroca_edubot': {'A0': 34, 'A1': 39, 'A2': 36, 'A3': 33, 'D0': 4, 'D1': 16, 'D2': 17, + 'D3': 22, 'D4': 23, 'D5': 5, 'D6': 18, 'D7': 19, 'D8': 33, 'LED': 13, + 'MOSI': 18, 'RX': 16, 'SCK': 5, 'SDA': 23, 'SS': 2, 'TX': 17, 'VBAT': 35}, 'pico32': {}, 'pocket_32': {'LED': 16}, + 'pycom_gpy': {'A1': 37, 'A2': 38, 'LED': 0, 'MISO': 37, 'MOSI': 22, 'SCK': 13, 'SCL': 13, + 'SDA': 12, 'SS': 17}, 'quantum': {}, - 'ttgo-lora32-v1': {'LED': 2, 'BUTTON': 0, 'SS': 18, 'MOSI': 27, 'SCK': 5, 'A1': 37, 'A2': 38, - 'OLED_SDA': 4, 'OLED_SCL': 15, 'OLED_RST': 16, 'LORA_SCK': 5, - 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 18, 'LORA_RST': 14, - 'LORA_IRQ': 26}, - 'ttgo-t-beam': {'LORA_SCK': 5, 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 18, 'LORA_RST': 23, - 'LORA_IRQ': 26, 'LORA_IO1': 33, 'LORA_IO2': 32, 'SS': 18, 'MOSI': 27, 'SCK': 5, - 'T8': 32, 'T9': 33, 'DAC2': 25}, - 'turta_iot_node': {'LED': 13, 'TX': 10, 'RX': 9, 'SDA': 23, 'SS': 21, 'MOSI': 18, 'SCK': 5, - 'A0': 4, 'A1': 25, 'A2': 26, 'A3': 27, 'A8': 38, 'T1': 25, 'T2': 26, - 'T3': 27, 'T4': 32, 'T5': 33, 'T6': 34, 'T7': 35, 'T8': 22, 'T9': 23, - 'T10': 10, 'T11': 9, 'T12': 21, 'T13': 5, 'T14': 18, 'T15': 19, - 'T16': 37, 'T17': 14, 'T18': 2, 'T19': 38}, - 'wemosbat': 'pocket_32', - 'wesp32': {'SCL': 4, 'SDA': 2, 'MISO': 32, 'ETH_PHY_ADDR': 0, 'ETH_PHY_MDC': 16, - 'ETH_PHY_MDIO': 17}, - 'widora-air': {'LED': 25, 'BUTTON': 0, 'SDA': 23, 'SCL': 19, 'MOSI': 16, 'MISO': 17, 'A1': 39, - 'A2': 35, 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, 'A9': 2, - 'D0': 19, 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5, 'D6': 4, 'T0': 19, - 'T1': 23, 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4}, + 'sparkfun_lora_gateway_1-channel': {'MISO': 12, 'MOSI': 13, 'SCK': 14, 'SS': 16}, + 'tinypico': {}, + 'ttgo-lora32-v1': {'A1': 37, 'A2': 38, 'BUTTON': 0, 'LED': 2, '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-t1': {'LED': 22, 'MISO': 2, 'MOSI': 15, 'SCK': 14, 'SCL': 23, 'SS': 13}, + 'turta_iot_node': {}, + 'vintlabs-devkit-v1': {'LED': 2, 'PWM0': 12, 'PWM1': 13, 'PWM2': 14, 'PWM3': 15, 'PWM4': 16, + 'PWM5': 17, 'PWM6': 18, 'PWM7': 19}, + 'wemos_d1_mini32': {'D0': 26, 'D1': 22, 'D2': 21, 'D3': 17, 'D4': 16, 'D5': 18, 'D6': 19, + 'D7': 23, 'D8': 5, 'LED': 2, 'RXD': 3, 'TXD': 1, '_VBAT': 35}, + 'wemosbat': {'LED': 16}, + 'wesp32': {'MISO': 32, 'SCL': 4, 'SDA': 15}, + 'widora-air': {'A1': 39, 'A2': 35, 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, + 'A9': 2, 'BUTTON': 0, 'D0': 19, 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, + 'D5': 5, 'D6': 4, 'LED': 25, 'MISO': 17, 'MOSI': 16, 'SCL': 19, 'SDA': 23, + 'T0': 19, 'T1': 23, 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4}, 'xinabox_cw02': {'LED': 27}, } diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 1656f1ad33..5cc4fad998 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -7,7 +7,7 @@ import re import subprocess from esphome.core import CORE -from esphome.py_compat import IS_PY2 +from esphome.py_compat import decode_text from esphome.util import run_external_command, run_external_process _LOGGER = logging.getLogger(__name__) @@ -18,15 +18,13 @@ def patch_structhash(): # removed/added. This might have unintended consequences, but this improves compile # times greatly when adding/removing components and a simple clean build solves # all issues - # pylint: disable=no-member,no-name-in-module - from platformio.commands import run - from platformio import util - from platformio.util import get_project_dir - from os.path import join, isdir, getmtime, isfile + from platformio.commands.run import helpers, command + from os.path import join, isdir, getmtime from os import makedirs - def patched_clean_build_dir(build_dir): - structhash_file = join(build_dir, "structure.hash") + def patched_clean_build_dir(build_dir, *args): + from platformio import util + from platformio.project.helpers import get_project_dir platformio_ini = join(get_project_dir(), "platformio.ini") # if project's config is modified @@ -36,27 +34,33 @@ def patch_structhash(): if not isdir(build_dir): makedirs(build_dir) - proj_hash = run.calculate_project_hash() - - # check project structure - if isdir(build_dir) and isfile(structhash_file): - with open(structhash_file) as f: - if f.read() == proj_hash: - return - - with open(structhash_file, "w") as f: - f.write(proj_hash) - # pylint: disable=protected-access - orig = run._clean_build_dir + helpers.clean_build_dir = patched_clean_build_dir + command.clean_build_dir = patched_clean_build_dir - def patched_safe(*args, **kwargs): - try: - return patched_clean_build_dir(*args, **kwargs) - except Exception: # pylint: disable=broad-except - return orig(*args, **kwargs) - run._clean_build_dir = patched_safe +IGNORE_LIB_WARNINGS = r'(?:' + '|'.join(['Hash', 'Update']) + r')' +FILTER_PLATFORMIO_LINES = [ + r'Verbose mode can be enabled via `-v, --verbose` option.*', + r'CONFIGURATION: https://docs.platformio.org/.*', + r'PLATFORM: .*', + r'DEBUG: Current.*', + r'PACKAGES: .*', + r'LDF: Library Dependency Finder -> http://bit.ly/configure-pio-ldf.*', + r'LDF Modes: Finder ~ chain, Compatibility ~ soft.*', + r'Looking for ' + IGNORE_LIB_WARNINGS + r' library in registry', + r"Warning! Library `.*'" + IGNORE_LIB_WARNINGS + + r".*` has not been found in PlatformIO Registry.", + r"You can ignore this message, if `.*" + IGNORE_LIB_WARNINGS + r".*` is a built-in library.*", + r'Scanning dependencies...', + r"Found \d+ compatible libraries", + r'Memory Usage -> http://bit.ly/pio-memory-usage', + r'esptool.py v.*', + r"Found: https://platformio.org/lib/show/.*", + r"Using cache: .*", + r'Installing dependencies', + r'.* @ .* is already installed', +] def run_platformio_cli(*args, **kwargs): @@ -65,18 +69,16 @@ def run_platformio_cli(*args, **kwargs): os.environ["PLATFORMIO_LIBDEPS_DIR"] = os.path.abspath(CORE.relative_piolibdeps_path()) cmd = ['platformio'] + list(args) - if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: - import platformio.__main__ - try: - if IS_PY2: - patch_structhash() - except Exception: # pylint: disable=broad-except - # Ignore when patch fails - pass - return run_external_command(platformio.__main__.main, - *cmd, **kwargs) + if not CORE.verbose: + kwargs['filter_lines'] = FILTER_PLATFORMIO_LINES - return run_external_process(*cmd, **kwargs) + if os.environ.get('ESPHOME_USE_SUBPROCESS') is not None: + return run_external_process(*cmd, **kwargs) + + import platformio.__main__ + patch_structhash() + return run_external_command(platformio.__main__.main, + *cmd, **kwargs) def run_platformio_cli_run(config, verbose, *args, **kwargs): @@ -98,6 +100,7 @@ def run_upload(config, verbose, port): def run_idedata(config): args = ['-t', 'idedata'] stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True) + stdout = decode_text(stdout) match = re.search(r'{.*}', stdout) if match is None: return IDEData(None) diff --git a/esphome/py_compat.py b/esphome/py_compat.py index 6833a55801..6cdaa5b047 100644 --- a/esphome/py_compat.py +++ b/esphome/py_compat.py @@ -1,5 +1,6 @@ import functools import sys +import codecs PYTHON_MAJOR = sys.version_info[0] IS_PY2 = PYTHON_MAJOR == 2 @@ -75,15 +76,14 @@ def indexbytes(buf, i): return ord(buf[i]) -if IS_PY2: - def decode_text(data, encoding='utf-8', errors='strict'): - # type: (str, str, str) -> unicode - if isinstance(data, unicode): - return data - return unicode(data, encoding=encoding, errors=errors) -else: - def decode_text(data, encoding='utf-8', errors='strict'): - # type: (bytes, str, str) -> str - if isinstance(data, str): - return data - return data.decode(encoding=encoding, errors=errors) +def decode_text(data, encoding='utf-8', errors='strict'): + if isinstance(data, text_type): + return data + return codecs.decode(data, encoding, errors) + + +def encode_text(data, encoding='utf-8', errors='strict'): + if isinstance(data, binary_type): + return data + + return codecs.encode(data, encoding, errors) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index b04f056f11..0305b59ef5 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -7,12 +7,13 @@ import os from esphome import const from esphome.core import CORE -from esphome.helpers import mkdir_p +from esphome.helpers import mkdir_p, write_file_if_changed # pylint: disable=unused-import, wrong-import-order from esphome.core import CoreType # noqa from typing import Any, Dict, Optional # noqa +from esphome.py_compat import text_type _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,7 @@ def trash_storage_path(base_path): # type: (str) -> str # pylint: disable=too-many-instance-attributes class StorageJSON(object): - def __init__(self, storage_version, name, esphome_version, + def __init__(self, storage_version, name, comment, esphome_version, src_version, arduino_version, address, esp_platform, board, build_path, firmware_bin_path, loaded_integrations): # Version of the storage JSON schema @@ -43,6 +44,8 @@ class StorageJSON(object): self.storage_version = storage_version # type: int # The name of the node self.name = name # type: str + # The comment of the node + self.comment = comment # type: str # The esphome version this was compiled with self.esphome_version = esphome_version # type: str # The version of the file in src/main.cpp - Used to migrate the file @@ -69,6 +72,7 @@ class StorageJSON(object): return { 'storage_version': self.storage_version, 'name': self.name, + 'comment': self.comment, 'esphome_version': self.esphome_version, 'src_version': self.src_version, 'arduino_version': self.arduino_version, @@ -85,14 +89,14 @@ class StorageJSON(object): def save(self, path): mkdir_p(os.path.dirname(path)) - with codecs.open(path, 'w', encoding='utf-8') as f_handle: - f_handle.write(self.to_json()) + write_file_if_changed(path, self.to_json()) @staticmethod def from_esphome_core(esph, old): # type: (CoreType, Optional[StorageJSON]) -> StorageJSON return StorageJSON( storage_version=1, name=esph.name, + comment=esph.comment, esphome_version=const.__version__, src_version=1, arduino_version=esph.arduino_version, @@ -110,6 +114,7 @@ class StorageJSON(object): return StorageJSON( storage_version=1, name=name, + comment=None, esphome_version=const.__version__, src_version=1, arduino_version=None, @@ -124,10 +129,10 @@ class StorageJSON(object): @staticmethod def _load_impl(path): # type: (str) -> Optional[StorageJSON] with codecs.open(path, 'r', encoding='utf-8') as f_handle: - text = f_handle.read() - storage = json.loads(text, encoding='utf-8') + storage = json.load(f_handle) storage_version = storage['storage_version'] name = storage.get('name') + comment = storage.get('comment') esphome_version = storage.get('esphome_version', storage.get('esphomeyaml_version')) src_version = storage.get('src_version') arduino_version = storage.get('arduino_version') @@ -137,7 +142,7 @@ class StorageJSON(object): build_path = storage.get('build_path') firmware_bin_path = storage.get('firmware_bin_path') loaded_integrations = storage.get('loaded_integrations', []) - return StorageJSON(storage_version, name, esphome_version, + return StorageJSON(storage_version, name, comment, esphome_version, src_version, arduino_version, address, esp_platform, board, build_path, firmware_bin_path, loaded_integrations) @@ -188,15 +193,12 @@ class EsphomeStorageJSON(object): return json.dumps(self.as_dict(), indent=2) + u'\n' def save(self, path): # type: (str) -> None - mkdir_p(os.path.dirname(path)) - with codecs.open(path, 'w', encoding='utf-8') as f_handle: - f_handle.write(self.to_json()) + write_file_if_changed(path, self.to_json()) @staticmethod def _load_impl(path): # type: (str) -> Optional[EsphomeStorageJSON] with codecs.open(path, 'r', encoding='utf-8') as f_handle: - text = f_handle.read() - storage = json.loads(text, encoding='utf-8') + storage = json.load(f_handle) storage_version = storage['storage_version'] cookie_secret = storage.get('cookie_secret') last_update_check = storage.get('last_update_check') @@ -215,7 +217,7 @@ class EsphomeStorageJSON(object): def get_default(): # type: () -> EsphomeStorageJSON return EsphomeStorageJSON( storage_version=1, - cookie_secret=binascii.hexlify(os.urandom(64)), + cookie_secret=text_type(binascii.hexlify(os.urandom(64))), last_update_check=None, remote_version=None, ) diff --git a/esphome/util.py b/esphome/util.py index 0ae0b9e32e..098d5e52da 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -3,12 +3,13 @@ from __future__ import print_function import collections import io import logging +import os import re import subprocess import sys from esphome import const -from esphome.py_compat import IS_PY2 +from esphome.py_compat import IS_PY2, decode_text, text_type _LOGGER = logging.getLogger(__name__) @@ -87,24 +88,67 @@ def shlex_quote(s): return u"'" + s.replace(u"'", u"'\"'\"'") + u"'" +ANSI_ESCAPE = re.compile(r'\033[@-_][0-?]*[ -/]*[@-~]') + + class RedirectText(object): - def __init__(self, out): + def __init__(self, out, filter_lines=None): self._out = out + if filter_lines is None: + self._filter_pattern = None + else: + pattern = r'|'.join(r'(?:' + pattern + r')' for pattern in filter_lines) + self._filter_pattern = re.compile(pattern) + self._line_buffer = '' def __getattr__(self, item): return getattr(self._out, item) - def write(self, s): + def _write_color_replace(self, s): from esphome.core import CORE if CORE.dashboard: - try: - s = s.replace('\033', '\\033') - except UnicodeEncodeError: - pass - + # With the dashboard, we must create a little hack to make color output + # work. The shell we create in the dashboard is not a tty, so python removes + # all color codes from the resulting stream. We just convert them to something + # we can easily recognize later here. + s = s.replace('\033', '\\033') self._out.write(s) + def write(self, s): + # s is usually a text_type already (self._out is of type TextIOWrapper) + # However, s is sometimes also a bytes object in python3. Let's make sure it's a + # text_type + # If the conversion fails, we will create an exception, which is okay because we won't + # be able to print it anyway. + text = decode_text(s) + assert isinstance(text, text_type) + + if self._filter_pattern is not None: + self._line_buffer += text + lines = self._line_buffer.splitlines(True) + for line in lines: + if '\n' not in line and '\r' not in line: + # Not a complete line, set line buffer + self._line_buffer = line + break + self._line_buffer = '' + + line_without_ansi = ANSI_ESCAPE.sub('', line) + line_without_end = line_without_ansi.rstrip() + if self._filter_pattern.match(line_without_end) is not None: + # Filter pattern matched, ignore the line + continue + + self._write_color_replace(line) + else: + self._write_color_replace(text) + + # write() returns the number of characters written + # Let's print the number of characters of the original string in order to not confuse + # any caller. + return len(s) + # pylint: disable=no-self-use def isatty(self): return True @@ -119,14 +163,15 @@ def run_external_command(func, *cmd, **kwargs): full_cmd = u' '.join(shlex_quote(x) for x in cmd) _LOGGER.info(u"Running: %s", full_cmd) + filter_lines = kwargs.get('filter_lines') orig_stdout = sys.stdout - sys.stdout = RedirectText(sys.stdout) + sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines) orig_stderr = sys.stderr - sys.stderr = RedirectText(sys.stderr) + sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines) capture_stdout = kwargs.get('capture_stdout', False) if capture_stdout: - cap_stdout = sys.stdout = io.BytesIO() + cap_stdout = sys.stdout = io.StringIO() try: sys.argv = list(cmd) @@ -154,14 +199,15 @@ def run_external_command(func, *cmd, **kwargs): def run_external_process(*cmd, **kwargs): full_cmd = u' '.join(shlex_quote(x) for x in cmd) _LOGGER.info(u"Running: %s", full_cmd) + filter_lines = kwargs.get('filter_lines') capture_stdout = kwargs.get('capture_stdout', False) if capture_stdout: sub_stdout = io.BytesIO() else: - sub_stdout = RedirectText(sys.stdout) + sub_stdout = RedirectText(sys.stdout, filter_lines=filter_lines) - sub_stderr = RedirectText(sys.stderr) + sub_stderr = RedirectText(sys.stderr, filter_lines=filter_lines) try: return subprocess.call(cmd, @@ -207,3 +253,16 @@ class OrderedDict(collections.OrderedDict): root[1] = first[0] = link else: super(OrderedDict, self).move_to_end(key, last=last) # pylint: disable=no-member + + +def list_yaml_files(folder): + files = filter_yaml_files([os.path.join(folder, p) for p in os.listdir(folder)]) + files.sort() + return files + + +def filter_yaml_files(files): + files = [f for f in files if os.path.splitext(f)[1] == '.yaml'] + files = [f for f in files if os.path.basename(f) != 'secrets.yaml'] + files = [f for f in files if not os.path.basename(f).startswith('.')] + return files diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 13f4c5d6b8..09fa1a6756 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -21,8 +21,8 @@ def ensure_multiple_invalid(err): # pylint: disable=protected-access, unidiomatic-typecheck class _Schema(vol.Schema): """Custom cv.Schema that prints similar keys on error.""" - def __init__(self, schema, extra=vol.PREVENT_EXTRA, extra_schemas=None): - super(_Schema, self).__init__(schema, extra=extra) + def __init__(self, schema, required=False, extra=vol.PREVENT_EXTRA, extra_schemas=None): + super(_Schema, self).__init__(schema, required=required, extra=extra) # List of extra schemas to apply after validation # Should be used sparingly, as it's not a very voluptuous-way/clean way of # doing things. diff --git a/esphome/vscode.py b/esphome/vscode.py index f0fa4ef52a..151bfb5281 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -59,7 +59,7 @@ def read_config(args): CORE.ace = args.ace f = data['file'] if CORE.ace: - CORE.config_path = os.path.join(args.configuration, f) + CORE.config_path = os.path.join(args.configuration[0], f) else: CORE.config_path = data['file'] vs = VSCodeResult() diff --git a/esphome/wizard.py b/esphome/wizard.py index ac77915497..67ec2c8960 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,13 +1,14 @@ from __future__ import print_function -import codecs import os +import random +import string import unicodedata import voluptuous as vol import esphome.config_validation as cv -from esphome.helpers import color, get_bool_env +from esphome.helpers import color, get_bool_env, write_file # pylint: disable=anomalous-backslash-in-string from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS from esphome.py_compat import safe_input, text_type @@ -52,6 +53,13 @@ wifi: ssid: "{ssid}" password: "{psk}" + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "{fallback_name}" + password: "{fallback_psk}" + +captive_portal: + # Enable logging logger: @@ -65,6 +73,14 @@ def sanitize_double_quotes(value): def wizard_file(**kwargs): + letters = string.ascii_letters + string.digits + ap_name_base = kwargs['name'].replace('_', ' ').title() + ap_name = "{} Fallback Hotspot".format(ap_name_base) + if len(ap_name) > 32: + ap_name = ap_name_base + kwargs['fallback_name'] = ap_name + kwargs['fallback_psk'] = ''.join(random.choice(letters) for _ in range(12)) + config = BASE_CONFIG.format(**kwargs) if kwargs['password']: @@ -87,8 +103,7 @@ def wizard_write(path, **kwargs): kwargs['platform'] = 'ESP8266' if board in ESP8266_BOARD_PINS else 'ESP32' platform = kwargs['platform'] - with codecs.open(path, 'w', 'utf-8') as f_handle: - f_handle.write(wizard_file(**kwargs)) + write_file(path, wizard_file(**kwargs)) storage = StorageJSON.from_wizard(name, name + '.local', platform, board) storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) storage.save(storage_path) @@ -117,8 +132,8 @@ def default_input(text, default): # From https://stackoverflow.com/a/518232/8924614 -def strip_accents(string): - return u''.join(c for c in unicodedata.normalize('NFD', text_type(string)) +def strip_accents(value): + return u''.join(c for c in unicodedata.normalize('NFD', text_type(value)) if unicodedata.category(c) != 'Mn') diff --git a/esphome/writer.py b/esphome/writer.py index bda3f09df2..4c2e03569f 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -8,7 +8,8 @@ from esphome.config import iter_components from esphome.const import CONF_BOARD_FLASH_MODE, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS, \ HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS, __version__ from esphome.core import CORE, EsphomeError -from esphome.helpers import mkdir_p, read_file, write_file_if_changed, walk_files +from esphome.helpers import mkdir_p, read_file, write_file_if_changed, walk_files, \ + copy_file_if_changed from esphome.storage_json import StorageJSON, storage_path _LOGGER = logging.getLogger(__name__) @@ -112,7 +113,7 @@ def migrate_src_version_0_to_1(): "auto-generated again.", main_cpp, main_cpp) _LOGGER.info("Migration: Added include section to %s", main_cpp) - write_file_if_changed(content, main_cpp) + write_file_if_changed(main_cpp, content) def migrate_src_version(old, new): @@ -171,9 +172,7 @@ def format_ini(data): def gather_lib_deps(): - lib_deps_l = [x.as_lib_dep for x in CORE.libraries] - lib_deps_l.sort() - return lib_deps_l + return [x.as_lib_dep for x in CORE.libraries] def gather_build_flags(): @@ -193,7 +192,7 @@ def get_ini_content(): 'framework': 'arduino', 'lib_deps': lib_deps + ['${common.lib_deps}'], 'build_flags': build_flags + ['${common.build_flags}'], - 'upload_speed': UPLOAD_SPEED_OVERRIDE.get(CORE.board, 115200), + 'upload_speed': UPLOAD_SPEED_OVERRIDE.get(CORE.board, 460800), } if CORE.is_esp32: @@ -253,7 +252,7 @@ def write_platformio_ini(content): content_format = INI_BASE_FORMAT full_file = content_format[0] + INI_AUTO_GENERATE_BEGIN + '\n' + content full_file += INI_AUTO_GENERATE_END + content_format[1] - write_file_if_changed(full_file, path) + write_file_if_changed(path, full_file) def write_platformio_project(): @@ -287,7 +286,6 @@ or use the custom_components folder. def copy_src_tree(): - import filecmp import shutil source_files = {} @@ -323,9 +321,7 @@ def copy_src_tree(): os.remove(path) else: src_path = source_files_copy.pop(target) - if not filecmp.cmp(path, src_path): - # Files are not same, copy - shutil.copy(src_path, path) + copy_file_if_changed(src_path, path) # Now copy new files for target, src_path in source_files_copy.items(): @@ -334,14 +330,14 @@ def copy_src_tree(): shutil.copy(src_path, dst_path) # Finally copy defines - write_file_if_changed(generate_defines_h(), - CORE.relative_src_path('esphome', 'core', 'defines.h')) - write_file_if_changed(ESPHOME_README_TXT, - CORE.relative_src_path('esphome', 'README.txt')) - write_file_if_changed(ESPHOME_H_FORMAT.format(include_s), - CORE.relative_src_path('esphome.h')) - write_file_if_changed(VERSION_H_FORMAT.format(__version__), - CORE.relative_src_path('esphome', 'core', 'version.h')) + write_file_if_changed(CORE.relative_src_path('esphome', 'core', 'defines.h'), + generate_defines_h()) + write_file_if_changed(CORE.relative_src_path('esphome', 'README.txt'), + ESPHOME_README_TXT) + write_file_if_changed(CORE.relative_src_path('esphome.h'), + ESPHOME_H_FORMAT.format(include_s)) + write_file_if_changed(CORE.relative_src_path('esphome', 'core', 'version.h'), + VERSION_H_FORMAT.format(__version__)) def generate_defines_h(): @@ -367,7 +363,7 @@ def write_cpp(code_s): full_file = code_format[0] + CPP_INCLUDE_BEGIN + u'\n' + global_s + CPP_INCLUDE_END full_file += code_format[1] + CPP_AUTO_GENERATE_BEGIN + u'\n' + code_s + CPP_AUTO_GENERATE_END full_file += code_format[2] - write_file_if_changed(full_file, path) + write_file_if_changed(path, full_file) def clean_build(): diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 30e8bb82de..fb04d7d5b0 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -14,7 +14,7 @@ from esphome import core from esphome.config_helpers import read_config_file from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange from esphome.py_compat import text_type, IS_PY2 -from esphome.util import OrderedDict +from esphome.util import OrderedDict, filter_yaml_files _LOGGER = logging.getLogger(__name__) @@ -260,12 +260,12 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_list(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) return [_load_yaml_internal(f) for f in files] @_add_data_ref def construct_include_dir_merge_list(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) merged_list = [] for fname in files: loaded_yaml = _load_yaml_internal(fname) @@ -275,7 +275,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_named(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) mapping = OrderedDict() for fname in files: filename = os.path.splitext(os.path.basename(fname))[0] @@ -284,7 +284,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_merge_named(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) mapping = OrderedDict() for fname in files: loaded_yaml = _load_yaml_internal(fname) @@ -297,12 +297,6 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors return Lambda(text_type(node.value)) -def _filter_yaml_files(files): - files = [f for f in files if os.path.basename(f) != SECRET_YAML] - files = [f for f in files if not os.path.basename(f).startswith('.')] - return files - - ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int) ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float) ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary) diff --git a/platformio.ini b/platformio.ini index c5a0d9a21b..19b850c546 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,20 +4,20 @@ ; It's *not* used during runtime. [platformio] -env_default = livingroom8266 +default_envs = livingroom8266 src_dir = . include_dir = include [common] lib_deps = - AsyncTCP@1.0.3 - AsyncMqttClient@0.8.2 + AsyncTCP@1.1.1 + AsyncMqttClient-esphome@0.8.3 ArduinoJson-esphomelib@5.13.3 - ESP Async WebServer@1.1.1 - FastLED@3.2.0 - NeoPixelBus@2.4.1 - ESPAsyncTCP@1.2.0 - TinyGPSPlus@1.0.2 + ESPAsyncWebServer-esphome@1.2.5 + FastLED@3.2.9 + NeoPixelBus-esphome@2.5.2 + ESPAsyncTCP-esphome@1.2.2 + 1655@1.0.2 ; TinyGPSPlus (has name conflict) build_flags = -Wno-reorder -DUSE_WEB_SERVER @@ -29,15 +29,8 @@ build_flags = ; log messages src_filter = +<esphome> -[env:livingroom32] -platform = espressif32@1.6.0 -board = nodemcu-32s -framework = arduino -lib_deps = ${common.lib_deps} -build_flags = ${common.build_flags} -DUSE_ETHERNET -src_filter = ${common.src_filter} +<tests/livingroom32.cpp> - [env:livingroom8266] +; use Arduino framework v2.3.0 for clang-tidy (latest 2.5.2 breaks static code analysis, see #760) platform = espressif8266@1.8.0 board = nodemcuv2 framework = arduino @@ -47,3 +40,11 @@ lib_deps = Hash build_flags = ${common.build_flags} src_filter = ${common.src_filter} +<tests/livingroom8266.cpp> + +[env:livingroom32] +platform = espressif32@1.11.0 +board = nodemcu-32s +framework = arduino +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} -DUSE_ETHERNET +src_filter = ${common.src_filter} +<tests/livingroom32.cpp> diff --git a/requirements.txt b/requirements.txt index 178d3d4459..14ce062000 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -voluptuous>=0.11.5,<0.12 -PyYAML>=5.1,<6 -paho-mqtt>=1.4,<2 -colorlog>=4.0.2 -tornado>=5.1.1,<6 +voluptuous==0.11.7 +PyYAML==5.1.2 +paho-mqtt==1.4.0 +colorlog==4.0.2 +tornado==5.1.1 typing>=3.6.6;python_version<"3.5" -protobuf>=3.7,<3.8 -tzlocal>=1.5.1 -pyserial>=3.4,<4 -ifaddr>=0.1.6,<1 -platformio>=3.6.5 ; python_version<"3" -https://github.com/platformio/platformio-core/archive/develop.zip ; python_version>"3" -esptool>=2.6,<3 +protobuf==3.10.0 +tzlocal==2.0.0 +pytz==2019.3 +pyserial==3.4 +ifaddr==0.1.6 +platformio==4.0.3 +esptool==2.7 diff --git a/requirements_test.txt b/requirements_test.txt index 079b0bf85f..26f14434d8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,16 +1,17 @@ -voluptuous>=0.11.5,<0.12 -PyYAML>=5.1,<6 -paho-mqtt>=1.4,<2 -colorlog>=4.0.2 -tornado>=5.1.1,<6 -typing>=3.6.6 ; python_version<"3.5" -protobuf>=3.7,<3.8 -tzlocal>=1.5.1 -pyserial>=3.4,<4 -ifaddr>=0.1.6,<1 -platformio>=3.6.5 ; python_version<"3" -https://github.com/platformio/platformio-core/archive/develop.zip ; python_version>"3" -esptool>=2.6,<3 +voluptuous==0.11.7 +PyYAML==5.1.2 +paho-mqtt==1.4.0 +colorlog==4.0.2 +tornado==5.1.1 +typing>=3.6.6;python_version<"3.5" +protobuf==3.10.0 +tzlocal==2.0.0 +pytz==2019.3 +pyserial==3.4 +ifaddr==0.1.6 +platformio==4.0.3 +esptool==2.7 + pylint==1.9.4 ; python_version<"3" pylint==2.3.0 ; python_version>"3" flake8==3.6.0 diff --git a/script/.neopixelbus.patch b/script/.neopixelbus.patch deleted file mode 100644 index 8dc2e74b66..0000000000 --- a/script/.neopixelbus.patch +++ /dev/null @@ -1,26 +0,0 @@ ---- .piolibdeps/NeoPixelBus_ID547/src/internal/NeoEsp8266DmaMethod.h 2018-12-25 06:37:53.000000000 +0100 -+++ .piolibdeps/NeoPixelBus_ID547/src/internal/NeoEsp8266DmaMethod.h.2 2019-03-01 22:18:10.000000000 +0100 -@@ -169,7 +169,7 @@ - _i2sBufDesc[indexDesc].sub_sof = 0; - _i2sBufDesc[indexDesc].datalen = blockSize; - _i2sBufDesc[indexDesc].blocksize = blockSize; -- _i2sBufDesc[indexDesc].buf_ptr = (uint32_t)is2Buffer; -+ _i2sBufDesc[indexDesc].buf_ptr = is2Buffer; - _i2sBufDesc[indexDesc].unused = 0; - _i2sBufDesc[indexDesc].next_link_ptr = (uint32_t)&(_i2sBufDesc[indexDesc + 1]); - -@@ -329,11 +329,13 @@ - case NeoDmaState_Sending: - { - slc_queue_item* finished_item = (slc_queue_item*)SLCRXEDA; -+ uint32_t **ptr = reinterpret_cast<uint32_t **>(&finished_item); -+ uint32_t dat = *reinterpret_cast<uint32_t *>(ptr); - - // the data block had actual data sent - // point last state block to first state block thus - // just looping and not sending the data blocks -- (finished_item + 1)->next_link_ptr = (uint32_t)(finished_item); -+ (finished_item + 1)->next_link_ptr = dat; - - s_this->_dmaState = NeoDmaState_Idle; - } diff --git a/script/api_protobuf/api_options_pb2.py b/script/api_protobuf/api_options_pb2.py new file mode 100644 index 0000000000..52cbbde678 --- /dev/null +++ b/script/api_protobuf/api_options_pb2.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: api_options.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='api_options.proto', + package='', + syntax='proto2', + serialized_options=None, + serialized_pb=_b('\n\x11\x61pi_options.proto\x1a google/protobuf/descriptor.proto\"\x06\n\x04void*F\n\rAPISourceType\x12\x0f\n\x0bSOURCE_BOTH\x10\x00\x12\x11\n\rSOURCE_SERVER\x10\x01\x12\x11\n\rSOURCE_CLIENT\x10\x02:E\n\x16needs_setup_connection\x12\x1e.google.protobuf.MethodOptions\x18\x8e\x08 \x01(\x08:\x04true:C\n\x14needs_authentication\x12\x1e.google.protobuf.MethodOptions\x18\x8f\x08 \x01(\x08:\x04true:/\n\x02id\x12\x1f.google.protobuf.MessageOptions\x18\x8c\x08 \x01(\r:\x01\x30:M\n\x06source\x12\x1f.google.protobuf.MessageOptions\x18\x8d\x08 \x01(\x0e\x32\x0e.APISourceType:\x0bSOURCE_BOTH:/\n\x05ifdef\x12\x1f.google.protobuf.MessageOptions\x18\x8e\x08 \x01(\t:3\n\x03log\x12\x1f.google.protobuf.MessageOptions\x18\x8f\x08 \x01(\x08:\x04true:9\n\x08no_delay\x12\x1f.google.protobuf.MessageOptions\x18\x90\x08 \x01(\x08:\x05\x66\x61lse') + , + dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,]) + +_APISOURCETYPE = _descriptor.EnumDescriptor( + name='APISourceType', + full_name='APISourceType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='SOURCE_BOTH', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOURCE_SERVER', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOURCE_CLIENT', index=2, number=2, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=63, + serialized_end=133, +) +_sym_db.RegisterEnumDescriptor(_APISOURCETYPE) + +APISourceType = enum_type_wrapper.EnumTypeWrapper(_APISOURCETYPE) +SOURCE_BOTH = 0 +SOURCE_SERVER = 1 +SOURCE_CLIENT = 2 + +NEEDS_SETUP_CONNECTION_FIELD_NUMBER = 1038 +needs_setup_connection = _descriptor.FieldDescriptor( + name='needs_setup_connection', full_name='needs_setup_connection', index=0, + number=1038, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=True, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +NEEDS_AUTHENTICATION_FIELD_NUMBER = 1039 +needs_authentication = _descriptor.FieldDescriptor( + name='needs_authentication', full_name='needs_authentication', index=1, + number=1039, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=True, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +ID_FIELD_NUMBER = 1036 +id = _descriptor.FieldDescriptor( + name='id', full_name='id', index=2, + number=1036, type=13, cpp_type=3, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +SOURCE_FIELD_NUMBER = 1037 +source = _descriptor.FieldDescriptor( + name='source', full_name='source', index=3, + number=1037, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +IFDEF_FIELD_NUMBER = 1038 +ifdef = _descriptor.FieldDescriptor( + name='ifdef', full_name='ifdef', index=4, + number=1038, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +LOG_FIELD_NUMBER = 1039 +log = _descriptor.FieldDescriptor( + name='log', full_name='log', index=5, + number=1039, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=True, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +NO_DELAY_FIELD_NUMBER = 1040 +no_delay = _descriptor.FieldDescriptor( + name='no_delay', full_name='no_delay', index=6, + number=1040, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) + + +_VOID = _descriptor.Descriptor( + name='void', + full_name='void', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=55, + serialized_end=61, +) + +DESCRIPTOR.message_types_by_name['void'] = _VOID +DESCRIPTOR.enum_types_by_name['APISourceType'] = _APISOURCETYPE +DESCRIPTOR.extensions_by_name['needs_setup_connection'] = needs_setup_connection +DESCRIPTOR.extensions_by_name['needs_authentication'] = needs_authentication +DESCRIPTOR.extensions_by_name['id'] = id +DESCRIPTOR.extensions_by_name['source'] = source +DESCRIPTOR.extensions_by_name['ifdef'] = ifdef +DESCRIPTOR.extensions_by_name['log'] = log +DESCRIPTOR.extensions_by_name['no_delay'] = no_delay +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +void = _reflection.GeneratedProtocolMessageType('void', (_message.Message,), dict( + DESCRIPTOR = _VOID, + __module__ = 'api_options_pb2' + # @@protoc_insertion_point(class_scope:void) + )) +_sym_db.RegisterMessage(void) + +google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(needs_setup_connection) +google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(needs_authentication) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(id) +source.enum_type = _APISOURCETYPE +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(source) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(ifdef) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(log) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(no_delay) + +# @@protoc_insertion_point(module_scope) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py new file mode 100644 index 0000000000..6357ae38ed --- /dev/null +++ b/script/api_protobuf/api_protobuf.py @@ -0,0 +1,867 @@ +"""Python 3 script to automatically generate C++ classes for ESPHome's native API. + +It's pretty crappy spaghetti code, but it works. +""" + +import re +from pathlib import Path +from textwrap import dedent +from subprocess import call + +# Generate with +# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto +import api_options_pb2 as pb +import google.protobuf.descriptor_pb2 as descriptor + +cwd = Path(__file__).parent +root = cwd.parent.parent / 'esphome' / 'components' / 'api' +prot = cwd / 'api.protoc' +call(['protoc', '-o', prot, '-I', root, 'api.proto']) +content = prot.read_bytes() + +d = descriptor.FileDescriptorSet.FromString(content) + + +def indent_list(text, padding=u' '): + return [padding + line for line in text.splitlines()] + + +def indent(text, padding=u' '): + return u'\n'.join(indent_list(text, padding)) + + +def camel_to_snake(name): + # https://stackoverflow.com/a/1176023 + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + +class TypeInfo(): + def __init__(self, field): + self._field = field + + @property + def default_value(self): + return '' + + @property + def name(self): + return self._field.name + + @property + def arg_name(self): + return self.name + + @property + def field_name(self): + return self.name + + @property + def number(self): + return self._field.number + + @property + def repeated(self): + return self._field.label == 3 + + @property + def cpp_type(self): + raise NotImplementedError + + @property + def reference_type(self): + return f'{self.cpp_type} ' + + @property + def const_reference_type(self): + return f'{self.cpp_type} ' + + @property + def public_content(self) -> str: + return [self.class_member] + + @property + def protected_content(self) -> str: + return [] + + @property + def class_member(self) -> str: + return f'{self.cpp_type} {self.field_name}{{{self.default_value}}}; // NOLINT' + + @property + def decode_varint_content(self) -> str: + content = self.decode_varint + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name} = {content}; + return true; + }}''') + + decode_varint = None + + @property + def decode_length_content(self) -> str: + content = self.decode_length + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name} = {content}; + return true; + }}''') + + decode_length = None + + @property + def decode_32bit_content(self) -> str: + content = self.decode_32bit + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name} = {content}; + return true; + }}''') + + decode_32bit = None + + @property + def decode_64bit_content(self) -> str: + content = self.decode_64bit + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name} = {content}; + return true; + }}''') + + decode_64bit = None + + @property + def encode_content(self): + return f'buffer.{self.encode_func}({self.number}, this->{self.field_name});' + + encode_func = None + + @property + def dump_content(self): + o = f'out.append(" {self.name}: ");\n' + o += self.dump(f'this->{self.field_name}') + '\n' + o += f'out.append("\\n");\n' + return o + + dump = None + + +TYPE_INFO = {} + + +def register_type(name): + def func(value): + TYPE_INFO[name] = value + return value + + return func + + +@register_type(1) +class DoubleType(TypeInfo): + cpp_type = 'double' + default_value = '0.0' + decode_64bit = 'value.as_double()' + encode_func = 'encode_double' + + def dump(self, name): + o = f'sprintf(buffer, "%g", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(2) +class FloatType(TypeInfo): + cpp_type = 'float' + default_value = '0.0f' + decode_32bit = 'value.as_float()' + encode_func = 'encode_float' + + def dump(self, name): + o = f'sprintf(buffer, "%g", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(3) +class Int64Type(TypeInfo): + cpp_type = 'int64_t' + default_value = '0' + decode_varint = 'value.as_int64()' + encode_func = 'encode_int64' + + def dump(self, name): + o = f'sprintf(buffer, "%ll", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(4) +class UInt64Type(TypeInfo): + cpp_type = 'uint64_t' + default_value = '0' + decode_varint = 'value.as_uint64()' + encode_func = 'encode_uint64' + + def dump(self, name): + o = f'sprintf(buffer, "%ull", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(5) +class Int32Type(TypeInfo): + cpp_type = 'int32_t' + default_value = '0' + decode_varint = 'value.as_int32()' + encode_func = 'encode_int32' + + def dump(self, name): + o = f'sprintf(buffer, "%d", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(6) +class Fixed64Type(TypeInfo): + cpp_type = 'uint64_t' + default_value = '0' + decode_64bit = 'value.as_fixed64()' + encode_func = 'encode_fixed64' + + def dump(self, name): + o = f'sprintf(buffer, "%ull", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(7) +class Fixed32Type(TypeInfo): + cpp_type = 'uint32_t' + default_value = '0' + decode_32bit = 'value.as_fixed32()' + encode_func = 'encode_fixed32' + + def dump(self, name): + o = f'sprintf(buffer, "%u", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(8) +class BoolType(TypeInfo): + cpp_type = 'bool' + default_value = 'false' + decode_varint = 'value.as_bool()' + encode_func = 'encode_bool' + + def dump(self, name): + o = f'out.append(YESNO({name}));' + return o + + +@register_type(9) +class StringType(TypeInfo): + cpp_type = 'std::string' + default_value = '' + reference_type = 'std::string &' + const_reference_type = 'const std::string &' + decode_length = 'value.as_string()' + encode_func = 'encode_string' + + def dump(self, name): + o = f'out.append("\'").append({name}).append("\'");' + return o + + +@register_type(11) +class MessageType(TypeInfo): + @property + def cpp_type(self): + return self._field.type_name[1:] + + default_value = '' + + @property + def reference_type(self): + return f'{self.cpp_type} &' + + @property + def const_reference_type(self): + return f'const {self.cpp_type} &' + + @property + def encode_func(self): + return f'encode_message<{self.cpp_type}>' + + @property + def decode_length(self): + return f'value.as_message<{self.cpp_type}>()' + + def dump(self, name): + o = f'{name}.dump_to(out);' + return o + + +@register_type(12) +class BytesType(TypeInfo): + cpp_type = 'std::string' + default_value = '' + reference_type = 'std::string &' + const_reference_type = 'const std::string &' + decode_length = 'value.as_string()' + encode_func = 'encode_string' + + def dump(self, name): + o = f'out.append("\'").append({name}).append("\'");' + return o + + +@register_type(13) +class UInt32Type(TypeInfo): + cpp_type = 'uint32_t' + default_value = '0' + decode_varint = 'value.as_uint32()' + encode_func = 'encode_uint32' + + def dump(self, name): + o = f'sprintf(buffer, "%u", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(14) +class EnumType(TypeInfo): + @property + def cpp_type(self): + return "Enum" + self._field.type_name[1:] + + @property + def decode_varint(self): + return f'value.as_enum<{self.cpp_type}>()' + + default_value = '' + + @property + def encode_func(self): + return f'encode_enum<{self.cpp_type}>' + + def dump(self, name): + o = f'out.append(proto_enum_to_string<{self.cpp_type}>({name}));' + return o + + +@register_type(15) +class SFixed32Type(TypeInfo): + cpp_type = 'int32_t' + default_value = '0' + decode_32bit = 'value.as_sfixed32()' + encode_func = 'encode_sfixed32' + + def dump(self, name): + o = f'sprintf(buffer, "%d", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(16) +class SFixed64Type(TypeInfo): + cpp_type = 'int64_t' + default_value = '0' + decode_64bit = 'value.as_sfixed64()' + encode_func = 'encode_sfixed64' + + def dump(self, name): + o = f'sprintf(buffer, "%ll", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(17) +class SInt32Type(TypeInfo): + cpp_type = 'int32_t' + default_value = '0' + decode_varint = 'value.as_sint32()' + encode_func = 'encode_sint32' + + def dump(self, name): + o = f'sprintf(buffer, "%d", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(18) +class SInt64Type(TypeInfo): + cpp_type = 'int64_t' + default_value = '0' + decode_varint = 'value.as_sint64()' + encode_func = 'encode_sin64' + + def dump(self): + o = f'sprintf(buffer, "%ll", {name});\n' + o += f'out.append(buffer);' + return o + + +class RepeatedTypeInfo(TypeInfo): + def __init__(self, field): + super(RepeatedTypeInfo, self).__init__(field) + self._ti = TYPE_INFO[field.type](field) + + @property + def cpp_type(self): + return f'std::vector<{self._ti.cpp_type}>' + + @property + def reference_type(self): + return f'{self.cpp_type} &' + + @property + def const_reference_type(self): + return f'const {self.cpp_type} &' + + @property + def decode_varint_content(self) -> str: + content = self._ti.decode_varint + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name}.push_back({content}); + return true; + }}''') + + @property + def decode_length_content(self) -> str: + content = self._ti.decode_length + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name}.push_back({content}); + return true; + }}''') + + @property + def decode_32bit_content(self) -> str: + content = self._ti.decode_32bit + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name}.push_back({content}); + return true; + }}''') + + @property + def decode_64bit_content(self) -> str: + content = self._ti.decode_64bit + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name}.push_back({content}); + return true; + }}''') + + @property + def _ti_is_bool(self): + # std::vector is specialized for bool, reference does not work + return isinstance(self._ti, BoolType) + + @property + def encode_content(self): + return f"""\ + for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{ + buffer.{self._ti.encode_func}({self.number}, it, true); + }}""" + + @property + def dump_content(self): + o = f'for (const auto {"" if self._ti_is_bool else "&"}it : this->{self.field_name}) {{\n' + o += f' out.append(" {self.name}: ");\n' + o += indent(self._ti.dump('it')) + '\n' + o += f' out.append("\\n");\n' + o += f'}}\n' + return o + + +def build_enum_type(desc): + name = "Enum" + desc.name + out = f"enum {name} : uint32_t {{\n" + for v in desc.value: + out += f' {v.name} = {v.number},\n' + out += '};\n' + + cpp = f"template<>\n" + cpp += f"const char *proto_enum_to_string<{name}>({name} value) {{\n" + cpp += f" switch (value) {{\n" + for v in desc.value: + cpp += f' case {v.name}: return "{v.name}";\n' + cpp += f' default: return "UNKNOWN";\n' + cpp += f' }}\n' + cpp += f'}}\n' + + return out, cpp + + +def build_message_type(desc): + public_content = [] + protected_content = [] + decode_varint = [] + decode_length = [] + decode_32bit = [] + decode_64bit = [] + encode = [] + dump = [] + + for field in desc.field: + if field.label == 3: + ti = RepeatedTypeInfo(field) + else: + ti = TYPE_INFO[field.type](field) + protected_content.extend(ti.protected_content) + public_content.extend(ti.public_content) + encode.append(ti.encode_content) + + if ti.decode_varint_content: + decode_varint.append(ti.decode_varint_content) + if ti.decode_length_content: + decode_length.append(ti.decode_length_content) + if ti.decode_32bit_content: + decode_32bit.append(ti.decode_32bit_content) + if ti.decode_64bit_content: + decode_64bit.append(ti.decode_64bit_content) + if ti.dump_content: + dump.append(ti.dump_content) + + cpp = '' + if decode_varint: + decode_varint.append('default:\n return false;') + o = f'bool {desc.name}::decode_varint(uint32_t field_id, ProtoVarInt value) {{\n' + o += ' switch (field_id) {\n' + o += indent("\n".join(decode_varint), ' ') + '\n' + o += ' }\n' + o += '}\n' + cpp += o + prot = 'bool decode_varint(uint32_t field_id, ProtoVarInt value) override;' + protected_content.insert(0, prot) + if decode_length: + decode_length.append('default:\n return false;') + o = f'bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n' + o += ' switch (field_id) {\n' + o += indent("\n".join(decode_length), ' ') + '\n' + o += ' }\n' + o += '}\n' + cpp += o + prot = 'bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;' + protected_content.insert(0, prot) + if decode_32bit: + decode_32bit.append('default:\n return false;') + o = f'bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n' + o += ' switch (field_id) {\n' + o += indent("\n".join(decode_32bit), ' ') + '\n' + o += ' }\n' + o += '}\n' + cpp += o + prot = 'bool decode_32bit(uint32_t field_id, Proto32Bit value) override;' + protected_content.insert(0, prot) + if decode_64bit: + decode_64bit.append('default:\n return false;') + o = f'bool {desc.name}::decode_64bit(uint32_t field_id, Proto64bit value) {{\n' + o += ' switch (field_id) {\n' + o += indent("\n".join(decode_64bit), ' ') + '\n' + o += ' }\n' + o += '}\n' + cpp += o + prot = 'bool decode_64bit(uint32_t field_id, Proto64bit value) override;' + protected_content.insert(0, prot) + + o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{\n" + o += indent('\n'.join(encode)) + '\n' + o += '}\n' + cpp += o + prot = 'void encode(ProtoWriteBuffer buffer) const override;' + public_content.append(prot) + + o = f"void {desc.name}::dump_to(std::string &out) const {{\n" + if dump: + o += f" char buffer[64];\n" + o += f' out.append("{desc.name} {{\\n");\n' + o += indent('\n'.join(dump)) + '\n' + o += f' out.append("}}");\n' + else: + o += f' out.append("{desc.name} {{}}");\n' + o += '}\n' + cpp += o + prot = 'void dump_to(std::string &out) const override;' + public_content.append(prot) + + out = f"class {desc.name} : public ProtoMessage {{\n" + out += ' public:\n' + out += indent('\n'.join(public_content)) + '\n' + out += ' protected:\n' + out += indent('\n'.join(protected_content)) + '\n' + out += "};\n" + return out, cpp + + +file = d.file[0] +content = '''\ +#pragma once + +#include "proto.h" + +namespace esphome { +namespace api { + +''' + +cpp = '''\ +#include "api_pb2.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace api { + +''' + +for enum in file.enum_type: + s, c = build_enum_type(enum) + content += s + cpp += c + +mt = file.message_type + +for m in mt: + s, c = build_message_type(m) + content += s + cpp += c + +content += '''\ + +} // namespace api +} // namespace esphome +''' +cpp += '''\ + +} // namespace api +} // namespace esphome +''' + +with open(root / 'api_pb2.h', 'w') as f: + f.write(content) + +with open(root / 'api_pb2.cpp', 'w') as f: + f.write(cpp) + +SOURCE_BOTH = 0 +SOURCE_SERVER = 1 +SOURCE_CLIENT = 2 + +RECEIVE_CASES = {} + +class_name = 'APIServerConnectionBase' + +ifdefs = {} + + +def get_opt(desc, opt, default=None): + if not desc.options.HasExtension(opt): + return default + return desc.options.Extensions[opt] + + +def build_service_message_type(mt): + snake = camel_to_snake(mt.name) + id_ = get_opt(mt, pb.id) + if id_ is None: + return None + + source = get_opt(mt, pb.source, 0) + + ifdef = get_opt(mt, pb.ifdef) + log = get_opt(mt, pb.log, True) + nodelay = get_opt(mt, pb.no_delay, False) + hout = '' + cout = '' + + if ifdef is not None: + ifdefs[str(mt.name)] = ifdef + hout += f'#ifdef {ifdef}\n' + cout += f'#ifdef {ifdef}\n' + + if source in (SOURCE_BOTH, SOURCE_SERVER): + # Generate send + func = f'send_{snake}' + hout += f'bool {func}(const {mt.name} &msg);\n' + cout += f'bool {class_name}::{func}(const {mt.name} &msg) {{\n' + if log: + cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' + cout += f' this->set_nodelay({str(nodelay).lower()});\n' + cout += f' return this->send_message_<{mt.name}>(msg, {id_});\n' + cout += f'}}\n' + if source in (SOURCE_BOTH, SOURCE_CLIENT): + # Generate receive + func = f'on_{snake}' + hout += f'virtual void {func}(const {mt.name} &value){{}};\n' + case = '' + if ifdef is not None: + case += f'#ifdef {ifdef}\n' + case += f'{mt.name} msg;\n' + case += f'msg.decode(msg_data, msg_size);\n' + if log: + case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' + case += f'this->{func}(msg);\n' + if ifdef is not None: + case += f'#endif\n' + case += 'break;' + RECEIVE_CASES[id_] = case + + if ifdef is not None: + hout += f'#endif\n' + cout += f'#endif\n' + + return hout, cout + + +hpp = '''\ +#pragma once + +#include "api_pb2.h" +#include "esphome/core/defines.h" + +namespace esphome { +namespace api { + +''' + +cpp = '''\ +#include "api_pb2_service.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace api { + +static const char *TAG = "api.service"; + +''' + +hpp += f'class {class_name} : public ProtoService {{\n' +hpp += ' public:\n' + +for mt in file.message_type: + obj = build_service_message_type(mt) + if obj is None: + continue + hout, cout = obj + hpp += indent(hout) + '\n' + cpp += cout + +cases = list(RECEIVE_CASES.items()) +cases.sort() +hpp += ' protected:\n' +hpp += f' bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n' +out = f'bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n' +out += f' switch(msg_type) {{\n' +for i, case in cases: + c = f'case {i}: {{\n' + c += indent(case) + '\n' + c += f'}}' + out += indent(c, ' ') + '\n' +out += ' default: \n' +out += ' return false;\n' +out += ' }\n' +out += ' return true;\n' +out += '}\n' +cpp += out +hpp += '};\n' + +serv = file.service[0] +class_name = 'APIServerConnection' +hpp += '\n' +hpp += f'class {class_name} : public {class_name}Base {{\n' +hpp += ' public:\n' +hpp_protected = '' +cpp += '\n' + +m = serv.method[0] +for m in serv.method: + func = m.name + inp = m.input_type[1:] + ret = m.output_type[1:] + is_void = ret == 'void' + snake = camel_to_snake(inp) + on_func = f'on_{snake}' + needs_conn = get_opt(m, pb.needs_setup_connection, True) + needs_auth = get_opt(m, pb.needs_authentication, True) + + ifdef = ifdefs.get(inp, None) + + if ifdef is not None: + hpp += f'#ifdef {ifdef}\n' + hpp_protected += f'#ifdef {ifdef}\n' + cpp += f'#ifdef {ifdef}\n' + + hpp_protected += f' void {on_func}(const {inp} &msg) override;\n' + hpp += f' virtual {ret} {func}(const {inp} &msg) = 0;\n' + cpp += f'void {class_name}::{on_func}(const {inp} &msg) {{\n' + body = '' + if needs_conn: + body += 'if (!this->is_connection_setup()) {\n' + body += ' this->on_no_setup_connection();\n' + body += ' return;\n' + body += '}\n' + if needs_auth: + body += 'if (!this->is_authenticated()) {\n' + body += ' this->on_unauthenticated_access();\n' + body += ' return;\n' + body += '}\n' + + if is_void: + body += f'this->{func}(msg);\n' + else: + body += f'{ret} ret = this->{func}(msg);\n' + ret_snake = camel_to_snake(ret) + body += f'if (!this->send_{ret_snake}(ret)) {{\n' + body += f' this->on_fatal_error();\n' + body += '}\n' + cpp += indent(body) + '\n' + '}\n' + + if ifdef is not None: + hpp += f'#endif\n' + hpp_protected += f'#endif\n' + cpp += f'#endif\n' + +hpp += ' protected:\n' +hpp += hpp_protected +hpp += '};\n' + +hpp += '''\ + +} // namespace api +} // namespace esphome +''' +cpp += '''\ + +} // namespace api +} // namespace esphome +''' + +with open(root / 'api_pb2_service.h', 'w') as f: + f.write(hpp) + +with open(root / 'api_pb2_service.cpp', 'w') as f: + f.write(cpp) + +prot.unlink() diff --git a/script/ci-custom.py b/script/ci-custom.py index 27b45dbe27..922d94f2aa 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -7,6 +7,7 @@ import fnmatch import os.path import subprocess import sys +import re def find_all(a_str, sub): @@ -28,15 +29,18 @@ EXECUTABLE_BIT = { s[3].strip(): int(s[0]) for s in lines } files = [s[3].strip() for s in lines] +files = list(filter(os.path.exists, files)) files.sort() -file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico', - '.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg') +file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico', '.svg', + '.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg', + '.woff', '.woff2', '') cpp_include = ('*.h', '*.c', '*.cpp', '*.tcc') -ignore_types = ('.ico',) +ignore_types = ('.ico', '.woff', '.woff2', '') LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] +LINT_POST_CHECKS = [] def run_check(lint_obj, fname, *args): @@ -82,6 +86,31 @@ def lint_content_check(**kwargs): return decorator +def lint_post_check(func): + _add_check(LINT_POST_CHECKS, func) + return func + + +def lint_re_check(regex, **kwargs): + prog = re.compile(regex, re.MULTILINE) + decor = lint_content_check(**kwargs) + + def decorator(func): + def new_func(fname, content): + errors = [] + for match in prog.finditer(content): + if 'NOLINT' in match.group(0): + continue + lineno = content.count("\n", 0, match.start()) + 1 + err = func(fname, match) + if err is None: + continue + errors.append("{} See line {}.".format(err, lineno)) + return errors + return decor(new_func) + return decorator + + def lint_content_find_check(find, **kwargs): decor = lint_content_check(**kwargs) @@ -90,9 +119,12 @@ def lint_content_find_check(find, **kwargs): find_ = find if callable(find): find_ = find(fname, content) + errors = [] for line, col in find_all(content, find_): err = func(fname) - return "{err} See line {line}:{col}.".format(err=err, line=line+1, col=col+1) + errors.append("{err} See line {line}:{col}." + "".format(err=err, line=line+1, col=col+1)) + return errors return decor(new_func) return decorator @@ -104,7 +136,7 @@ def lint_ino(fname): @lint_file_check(exclude=['*{}'.format(f) for f in file_types] + [ '.clang-*', '.dockerignore', '.editorconfig', '*.gitignore', 'LICENSE', 'pylintrc', - 'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*' + 'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*', ]) def lint_ext_check(fname): return "This file extension is not a registered file type. If this is an error, please " \ @@ -124,7 +156,6 @@ def lint_executable_bit(fname): @lint_content_find_check('\t', exclude=[ 'esphome/dashboard/static/ace.js', 'esphome/dashboard/static/ext-searchbox.js', - 'script/.neopixelbus.patch', ]) def lint_tabs(fname): return "File contains tab character. Please convert tabs to spaces." @@ -135,13 +166,105 @@ def lint_newline(fname): return "File contains windows newline. Please set your editor to unix newline mode." -@lint_content_check() +@lint_content_check(exclude=['*.svg']) def lint_end_newline(fname, content): if content and not content.endswith('\n'): return "File does not end with a newline, please add an empty line at the end of the file." return None +CPP_RE_EOL = r'\s*?(?://.*?)?$' + + +def highlight(s): + return '\033[36m{}\033[0m'.format(s) + + +@lint_re_check(r'^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)' + CPP_RE_EOL, + include=cpp_include, exclude=['esphome/core/log.h']) +def lint_no_defines(fname, match): + s = highlight('static const uint8_t {} = {};'.format(match.group(1), match.group(2))) + return ("#define macros for integer constants are not allowed, please use " + "{} style instead (replace uint8_t with the appropriate " + "datatype). See also Google style guide.".format(s)) + + +@lint_re_check(r'^\s*delay\((\d+)\);' + CPP_RE_EOL, include=cpp_include) +def lint_no_long_delays(fname, match): + duration_ms = int(match.group(1)) + if duration_ms < 50: + return None + return ( + "{} - long calls to delay() are not allowed in ESPHome because everything executes " + "in one thread. Calling delay() will block the main thread and slow down ESPHome.\n" + "If there's no way to work around the delay() and it doesn't execute often, please add " + "a '// NOLINT' comment to the line." + "".format(highlight(match.group(0).strip())) + ) + + +@lint_content_check(include=['esphome/const.py']) +def lint_const_ordered(fname, content): + lines = content.splitlines() + errors = [] + for start in ['CONF_', 'ICON_', 'UNIT_']: + matching = [(i+1, line) for i, line in enumerate(lines) if line.startswith(start)] + ordered = list(sorted(matching, key=lambda x: x[1].replace('_', ' '))) + ordered = [(mi, ol) for (mi, _), (_, ol) in zip(matching, ordered)] + for (mi, ml), (oi, ol) in zip(matching, ordered): + if ml == ol: + continue + target = next(i for i, l in ordered if l == ml) + target_text = next(l for i, l in matching if target == i) + errors.append("Constant {} is not ordered, please make sure all constants are ordered. " + "See line {} (should go to line {}, {})" + "".format(highlight(ml), mi, target, target_text)) + return errors + + +@lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=['*.py']) +def lint_conf_matches(fname, match): + const = match.group(1) + value = match.group(2) + const_norm = const.lower() + value_norm = value.replace('.', '_') + if const_norm == value_norm: + return None + return ("Constant {} does not match value {}! Please make sure the constant's name matches its " + "value!" + "".format(highlight('CONF_' + const), highlight(value))) + + +CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$' +with codecs.open('esphome/const.py', 'r', encoding='utf-8') as f_handle: + constants_content = f_handle.read() +CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)] + +CONSTANTS_USES = collections.defaultdict(list) + + +@lint_re_check(CONF_RE, include=['*.py'], exclude=['esphome/const.py']) +def lint_conf_from_const_py(fname, match): + name = match.group(1) + if name not in CONSTANTS: + CONSTANTS_USES[name].append(fname) + return None + return ("Constant {} has already been defined in const.py - please import the constant from " + "const.py directly.".format(highlight(name))) + + +@lint_post_check +def lint_constants_usage(): + errors = [] + for constant, uses in CONSTANTS_USES.items(): + if len(uses) < 4: + continue + errors.append("Constant {} is defined in {} files. Please move all definitions of the " + "constant to const.py (Uses: {})" + "".format(highlight(constant), len(uses), ', '.join(uses))) + return errors + + def relative_cpp_search_text(fname, content): parts = fname.split('/') integration = parts[2] @@ -164,7 +287,8 @@ def relative_py_search_text(fname, content): return 'esphome.components.{}'.format(integration) -@lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py']) +@lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py'], + exclude=['esphome/components/web_server/__init__.py']) def lint_relative_py_import(fname): return ("Component contains absolute import - Components must always use " "relative imports within the integration.\n" @@ -174,6 +298,19 @@ def lint_relative_py_import(fname): ' from . import abc_ns\n\n') +@lint_content_check(include=['esphome/components/*.h', 'esphome/components/*.cpp', + 'esphome/components/*.tcc']) +def lint_namespace(fname, content): + expected_name = re.match(r'^esphome/components/([^/]+)/.*', + fname.replace(os.path.sep, '/')).group(1) + search = 'namespace {}'.format(expected_name) + if search in content: + return None + return 'Invalid namespace found in C++ file. All integration C++ files should put all ' \ + 'functions in a separate namespace that matches the integration\'s name. ' \ + 'Please make sure the file contains {}'.format(highlight(search)) + + @lint_content_find_check('"esphome.h"', include=cpp_include, exclude=['tests/custom.h']) def lint_esphome_h(fname): return ("File contains reference to 'esphome.h' - This file is " @@ -201,6 +338,7 @@ def lint_pragma_once(fname, content): 'esphome/components/stepper/stepper.h', 'esphome/components/switch/switch.h', 'esphome/components/text_sensor/text_sensor.h', + 'esphome/components/climate/climate.h', 'esphome/core/component.h', 'esphome/core/esphal.h', 'esphome/core/log.h', @@ -229,7 +367,7 @@ def add_errors(fname, errs): for fname in files: _, ext = os.path.splitext(fname) run_checks(LINT_FILE_CHECKS, fname, fname) - if ext in ('.ico',): + if ext in ignore_types: continue try: with codecs.open(fname, 'r', encoding='utf-8') as f_handle: @@ -239,6 +377,8 @@ for fname in files: continue run_checks(LINT_CONTENT_CHECKS, fname, fname, content) +run_checks(LINT_POST_CHECKS, 'POST') + for f, errs in sorted(errors.items()): print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f)) for err in errs: diff --git a/script/clang-tidy b/script/clang-tidy index 39df87df22..f178e036b1 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -54,7 +54,6 @@ def run_tidy(args, tmpdir, queue, lock, failed_files): if rc != 0: print() print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path)) - print(invocation_s) print(output) print() failed_files.append(path) diff --git a/script/lint-cpp b/script/lint-cpp index 88728c9abd..170d61d539 100755 --- a/script/lint-cpp +++ b/script/lint-cpp @@ -6,9 +6,6 @@ cd "$(dirname "$0")/.." if [[ ! -e ".gcc-flags.json" ]]; then pio init --ide atom fi -if ! patch -R -p0 -s -f --dry-run <script/.neopixelbus.patch; then - patch -p0 < script/.neopixelbus.patch -fi set -x diff --git a/script/quicklint b/script/quicklint index 06c31d519d..e391ca3276 100755 --- a/script/quicklint +++ b/script/quicklint @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash set -e diff --git a/script/setup b/script/setup index a65eb23a63..b6cff39f0c 100755 --- a/script/setup +++ b/script/setup @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash # Set up ESPHome dev environment set -e diff --git a/setup.py b/setup.py index a93cd0b325..53acef5a30 100755 --- a/setup.py +++ b/setup.py @@ -23,16 +23,17 @@ GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) DOWNLOAD_URL = '{}/archive/v{}.zip'.format(GITHUB_URL, const.__version__) REQUIRES = [ - 'voluptuous>=0.11.5,<0.12', - 'PyYAML>=5.1,<6', - 'paho-mqtt>=1.4,<2', - 'colorlog>=4.0.2', - 'tornado>=5.1.1,<6', + 'voluptuous==0.11.7', + 'PyYAML==5.1.2', + 'paho-mqtt==1.4.0', + 'colorlog==4.0.2', + 'tornado==5.1.1', 'typing>=3.6.6;python_version<"3.5"', - 'protobuf>=3.7,<3.8', - 'tzlocal>=1.5.1', - 'pyserial>=3.4,<4', - 'ifaddr>=0.1.6,<1', + 'protobuf==3.10.0', + 'tzlocal==2.0.0', + 'pytz==2019.3', + 'pyserial==3.4', + 'ifaddr==0.1.6', ] # If you have problems importing platformio and esptool as modules you can set @@ -40,8 +41,8 @@ REQUIRES = [ # This means they have to be in your $PATH. if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: REQUIRES.extend([ - 'platformio>=3.6.5', - 'esptool>=2.6,<3', + 'platformio==4.0.3', + 'esptool==2.7', ]) CLASSIFIERS = [ diff --git a/tests/custom.h b/tests/custom.h index b392819efb..278e300785 100644 --- a/tests/custom.h +++ b/tests/custom.h @@ -3,7 +3,12 @@ class CustomSensor : public Component, public Sensor { public: - void loop() override { publish_state(42.0); } + void loop() override { + // Test id() helper + float value = id(my_sensor).state; + id(my_global_string) = "Hello World"; + publish_state(42.0); + } }; class CustomTextSensor : public Component, public TextSensor { @@ -36,3 +41,32 @@ class CustomFloatOutput : public FloatOutput, public Component { protected: void write_state(float state) override { ESP_LOGD("custom_output", "Setting %f", state); } }; + +class CustomNativeAPI : public Component, public CustomAPIDevice { + public: + void setup() override { + register_service(&CustomNativeAPI::on_hello_world, "hello_world"); + register_service(&CustomNativeAPI::on_start_dryer, "start_dryer", {"value"}); + register_service(&CustomNativeAPI::on_many_values, "many_values", {"bool", "int", "float", "str1", "str2"}); + subscribe_homeassistant_state(&CustomNativeAPI::on_light_state, "light.my_light"); + } + + void on_hello_world() { ESP_LOGD("custom_api", "Hello World from native API service!"); } + void on_start_dryer(int value) { ESP_LOGD("custom_api", "Value is %d", value); } + void on_many_values(bool a_bool, int a_int, float a_float, std::string str1, std::string str2) { + ESP_LOGD("custom_api", "Bool: %s", ONOFF(a_bool)); + ESP_LOGD("custom_api", "Int: %d", a_int); + ESP_LOGD("custom_api", "Float: %f", a_float); + ESP_LOGD("custom_api", "String1: %s", str1.c_str()); + ESP_LOGD("custom_api", "String2: %s", str2.c_str()); + ESP_LOGD("custom_api", "Connected: %s", YESNO(is_connected())); + + call_homeassistant_service("light.turn_on", { + {"entity_id", "light.my_light"}, + {"brightness", "127"}, + }); + // no args + call_homeassistant_service("homeassistant.restart"); + } + void on_light_state(std::string state) { ESP_LOGD("custom_api", "Got state %s", state.c_str()); } +}; diff --git a/tests/test1.yaml b/tests/test1.yaml index fac39b6a06..66f0220836 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -73,10 +73,10 @@ mqtt: ESP_LOGD("main", "Got message %s", x.c_str()); - topic: livingroom/ota_mode then: - - deep_sleep.prevent: deep_sleep_1 + - deep_sleep.prevent - topic: livingroom/ota_mode then: - - deep_sleep.enter: deep_sleep_1 + - deep_sleep.enter: on_json_message: topic: the/topic then: @@ -163,7 +163,6 @@ deep_sleep: sleep_duration: 50s wakeup_pin: GPIO39 wakeup_pin_mode: INVERT_WAKEUP - id: deep_sleep_1 ads1115: address: 0x48 @@ -171,6 +170,10 @@ ads1115: dallas: pin: GPIO23 +as3935_spi: + cs_pin: GPIO12 + irq_pin: GPIO13 + sensor: - platform: adc pin: A0 @@ -182,6 +185,7 @@ sensor: accuracy_decimals: 5 expire_after: 120s setup_priority: -100 + force_update: true filters: - offset: 2.0 - multiply: 1.2 @@ -191,6 +195,10 @@ sensor: - 100.0 -> 102.5 - filter_out: 42.0 - filter_out: nan + - median: + window_size: 5 + send_every: 5 + send_first_at: 3 - sliding_window_moving_average: window_size: 15 send_every: 15 @@ -244,6 +252,35 @@ sensor: state_topic: hi/me retain: false availability: + - platform: atm90e32 + cs_pin: 5 + phase_a: + voltage: + name: "EMON Line Voltage A" + current: + name: "EMON CT1 Current" + power: + name: "EMON Active Power CT1" + gain_voltage: 47660 + gain_ct: 12577 + phase_b: + current: + name: "EMON CT2 Current" + power: + name: "EMON Active Power CT2" + gain_voltage: 47660 + gain_ct: 12577 + phase_c: + current: + name: "EMON CT3 Current" + power: + name: "EMON Active Power CT3" + gain_voltage: 47660 + gain_ct: 12577 + frequency: + name: "EMON Line Frequency" + line_frequency: 50Hz + gain_pga: 2X - platform: bh1750 name: "Living Room Brightness 3" internal: true @@ -347,6 +384,7 @@ sensor: current_resistor: 0.001 ohm voltage_divider: 2351 change_mode_every: 16 + initial_mode: VOLTAGE - platform: total_daily_energy power_id: hlw8012_power name: "HLW8012 Total Daily Energy" @@ -419,6 +457,7 @@ sensor: temperature: name: "MH-Z19 Temperature" update_interval: 15s + automatic_baseline_calibration: false - platform: mpu6050 address: 0x68 accel_x: @@ -452,6 +491,7 @@ sensor: update_interval: 15s - platform: rotary_encoder name: "Rotary Encoder" + id: rotary_encoder1 pin_a: GPIO23 pin_b: GPIO24 pin_reset: GPIO25 @@ -462,9 +502,20 @@ sensor: resolution: 4 min_value: -10 max_value: 30 + on_value: + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: 10 + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: !lambda 'return -1;' - platform: pulse_width name: Pulse Width pin: GPIO12 + - platform: senseair + co2: + name: "SenseAir CO2 Value" + update_interval: 15s - platform: sht3xd temperature: name: "Living Room Temperature 8" @@ -472,6 +523,34 @@ sensor: name: "Living Room Humidity 8" address: 0x44 update_interval: 15s + - platform: sts3x + name: "Living Room Temperature 9" + address: 0x4A + - platform: scd30 + co2: + name: "Living Room CO2 9" + temperature: + name: "Living Room Temperature 9" + humidity: + name: "Living Room Humidity 9" + address: 0x61 + update_interval: 15s + - platform: sgp30 + eco2: + name: "Workshop eCO2" + accuracy_decimals: 1 + tvoc: + name: "Workshop TVOC" + accuracy_decimals: 1 + address: 0x58 + update_interval: 5s + - platform: shtcx + temperature: + name: "Living Room Temperature 10" + humidity: + name: "Living Room Humidity 10" + address: 0x70 + update_interval: 15s - platform: template name: "Template Sensor" id: template_sensor @@ -534,7 +613,28 @@ sensor: name: CCS811 TVOC update_interval: 30s baseline: 0x4242 - + - platform: tx20 + wind_speed: + name: "Windspeed" + wind_direction_degrees: + name: "Winddirection Degrees" + pin: + number: GPIO04 + mode: INPUT + - platform: zyaura + clock_pin: GPIO5 + data_pin: GPIO4 + co2: + name: "ZyAura CO2" + temperature: + name: "ZyAura Temperature" + humidity: + name: "ZyAura Humidity" + - platform: as3935 + lightning_energy: + name: "Lightning Energy" + distance: + name: "Distance Storm" esp32_touch: setup_mode: False @@ -617,6 +717,7 @@ binary_sensor: name: "ESP32 Touch Pad GPIO27" pin: GPIO27 threshold: 1000 + id: btn_left - platform: nextion page_id: 0 component_id: 2 @@ -641,6 +742,12 @@ binary_sensor: - binary_sensor.template.publish: id: garage_door state: OFF + - output.ledc.set_frequency: + id: gpio_19 + frequency: 500.0Hz + - output.ledc.set_frequency: + id: gpio_19 + frequency: !lambda 'return 500.0;' - platform: pn532 uid: 74-10-37-94 name: "PN532 NFC Tag" @@ -655,12 +762,20 @@ binary_sensor: mode: INPUT inverted: True - platform: gpio - name: "MCP binary sensor" + name: "MCP21 binary sensor" pin: mcp23017: mcp23017_hub number: 1 mode: INPUT inverted: True + - platform: gpio + name: "MCP22 binary sensor" + pin: + mcp23008: mcp23008_hub + number: 7 + mode: INPUT_PULLUP + inverted: False + - platform: remote_receiver name: "Raw Remote Receiver Test" raw: @@ -668,11 +783,21 @@ binary_sensor: 3700, -2263, 1712, -4254, 1711, -4249, 1715, -2266, 1710, -2267, 1709, -2265, 3704, -4250, 1712, -4254, 3700, -2260, 1714, -2265, 1712, -2262, 1714, -2267, 1709] + - platform: as3935 + name: "Storm Alert" pca9685: frequency: 500 address: 0x0 +tlc59208f: + - address: 0x20 + id: tlc59208f_1 + - address: 0x22 + id: tlc59208f_2 + - address: 0x24 + id: tlc59208f_3 + my9231: data_pin: GPIO12 clock_pin: GPIO14 @@ -690,7 +815,6 @@ output: pin: 19 id: gpio_19 frequency: 1500Hz - bit_depth: 8 channel: 14 max_power: 0.5 - platform: pca9685 @@ -717,6 +841,42 @@ output: - platform: pca9685 id: pca_7 channel: 7 + - platform: tlc59208f + id: tlc_0 + channel: 0 + tlc59208f_id: 'tlc59208f_1' + - platform: tlc59208f + id: tlc_1 + channel: 1 + tlc59208f_id: 'tlc59208f_1' + - platform: tlc59208f + id: tlc_2 + channel: 2 + tlc59208f_id: 'tlc59208f_1' + - platform: tlc59208f + id: tlc_3 + channel: 0 + tlc59208f_id: 'tlc59208f_2' + - platform: tlc59208f + id: tlc_4 + channel: 1 + tlc59208f_id: 'tlc59208f_2' + - platform: tlc59208f + id: tlc_5 + channel: 2 + tlc59208f_id: 'tlc59208f_2' + - platform: tlc59208f + id: tlc_6 + channel: 0 + tlc59208f_id: 'tlc59208f_3' + - platform: tlc59208f + id: tlc_7 + channel: 1 + tlc59208f_id: 'tlc59208f_3' + - platform: tlc59208f + id: tlc_8 + channel: 2 + tlc59208f_id: 'tlc59208f_3' - platform: gpio id: id2 pin: @@ -731,6 +891,13 @@ output: number: 0 mode: OUTPUT inverted: False + - platform: gpio + id: id23 + pin: + mcp23008: mcp23008_hub + number: 0 + mode: OUTPUT + inverted: False - platform: my9231 id: my_0 channel: 0 @@ -862,6 +1029,11 @@ light: name: Flicker Effect With Custom Values update_interval: 16ms intensity: 5% + - addressable_lambda: + name: "Test For Custom Lambda Effect" + lambda: |- + it[0] = current_color; + - automation: name: Custom Effect sequence: @@ -982,7 +1154,7 @@ switch: name: RC Switch Raw turn_on_action: remote_transmitter.transmit_rc_switch_raw: - code: '001010011001111101011011' + code: '00101001100111110101xxxx' protocol: 1 - platform: template name: RC Switch Type A @@ -1124,6 +1296,16 @@ interval: if (true) return id(page1); else return id(page2); - display.page.show_next: display1 - display.page.show_previous: display1 + - interval: 2s + then: + - lambda: |- + static uint16_t btn_left_state = id(btn_left)->get_value(); + + ESP_LOGD("adaptive touch", "___ Touch Pad '%s' (T%u): val: %u state: %u tres:%u", id(btn_left)->get_name().c_str(), id(btn_left)->get_touch_pad(), id(btn_left)->get_value(), btn_left_state, id(btn_left)->get_threshold()); + + btn_left_state = ((uint32_t) id(btn_left)->get_value() + 63 * (uint32_t)btn_left_state) >> 6; + + id(btn_left)->set_threshold(btn_left_state * 0.9); display: - platform: lcd_gpio @@ -1152,10 +1334,11 @@ display: it.set_component_value("gauge", 50); it.set_component_text("textview", "Hello World!"); - platform: ssd1306_i2c - model: "SSD1306 128x64" + model: "SSD1306_128X64" reset_pin: GPIO23 address: 0x3C id: display1 + brightness: 60% pages: - id: page1 lambda: |- @@ -1170,6 +1353,13 @@ display: reset_pin: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); +- platform: ssd1325_spi + model: "SSD1325 128x64" + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper cs_pin: GPIO23 dc_pin: GPIO23 @@ -1241,6 +1431,10 @@ pcf8574: mcp23017: - id: 'mcp23017_hub' +mcp23008: + - id: 'mcp23008_hub' + address: 0x22 + stepper: - platform: a4988 id: my_stepper diff --git a/tests/test2.yaml b/tests/test2.yaml index e3a9b0da85..097c29285a 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -45,35 +45,56 @@ logger: level: DEBUG web_server: + auth: + username: admin + password: admin deep_sleep: run_duration: 20s sleep_duration: 50s +as3935_i2c: + irq_pin: GPIO12 + + sensor: - platform: ble_rssi mac_address: AC:37:43:77:5F:4C name: "BLE Google Home Mini RSSI value" - - platform: xiaomi_miflora + - platform: xiaomi_hhccjcy01 mac_address: 94:2B:FF:5C:91:61 temperature: - name: "Xiaomi MiFlora Temperature" + name: "Xiaomi HHCCJCY01 Temperature" moisture: - name: "Xiaomi MiFlora Moisture" + name: "Xiaomi HHCCJCY01 Moisture" illuminance: - name: "Xiaomi MiFlora Illuminance" + name: "Xiaomi HHCCJCY01 Illuminance" conductivity: - name: "Xiaomi MiFlora Soil Conductivity" + name: "Xiaomi HHCCJCY01 Soil Conductivity" battery_level: - name: "Xiaomi MiFlora Battery Level" - - platform: xiaomi_mijia + name: "Xiaomi HHCCJCY01 Battery Level" + - platform: xiaomi_lywsdcgq mac_address: 7A:80:8E:19:36:BA temperature: - name: "Xiaomi MiJia Temperature" + name: "Xiaomi LYWSDCGQ Temperature" humidity: - name: "Xiaomi MiJia Humidity" + name: "Xiaomi LYWSDCGQ Humidity" battery_level: - name: "Xiaomi MiJia Battery Level" + name: "Xiaomi LYWSDCGQ Battery Level" + - platform: xiaomi_lywsd02 + mac_address: 3F:5B:7D:82:58:4E + temperature: + name: "Xiaomi LYWSD02 Temperature" + humidity: + name: "Xiaomi LYWSD02 Humidity" + - platform: xiaomi_cgg1 + mac_address: 7A:80:8E:19:36:BA + temperature: + name: "Xiaomi CGG1 Temperature" + humidity: + name: "Xiaomi CGG1 Humidity" + battery_level: + name: "Xiaomi CGG1 Battery Level" - platform: pmsx003 type: PMSX003 pm_1_0: @@ -125,6 +146,11 @@ sensor: - platform: homeassistant entity_id: sensor.hello_world id: ha_hello_world + - platform: as3935 + lightning_energy: + name: "Lightning Energy" + distance: + name: "Distance Storm" time: - platform: homeassistant @@ -163,13 +189,14 @@ binary_sensor: - platform: homeassistant entity_id: binary_sensor.hello_world id: ha_hello_world_binary + - platform: as3935 + name: "Storm Alert" remote_receiver: pin: GPIO32 dump: [] esp32_ble_tracker: - scan_interval: 300s #esp32_ble_beacon: # type: iBeacon diff --git a/tests/test3.yaml b/tests/test3.yaml index 6b64af8981..d1596c8f41 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1,5 +1,6 @@ esphome: name: $devicename + comment: $devicecomment platform: ESP8266 board: d1_mini build_path: build/test3 @@ -12,6 +13,7 @@ esphome: substitutions: devicename: test3 + devicecomment: test3 device api: port: 8000 @@ -40,6 +42,101 @@ api: - stepper.set_target: id: my_stepper2 target: !lambda 'return int_;' + - service: array_types + variables: + bool_arr: bool[] + int_arr: int[] + float_arr: float[] + string_arr: string[] + then: + - logger.log: + format: 'Bool: %s (%u), Int: %d (%u), Float: %f (%u), String: %s (%u)' + args: + - YESNO(bool_arr[0]) + - bool_arr.size() + - int_arr[0] + - int_arr.size() + - float_arr[0] + - float_arr.size() + - string_arr[0].c_str() + - string_arr.size() + - service: dfplayer_next + then: + - dfplayer.play_next: + - service: dfplayer_previous + then: + - dfplayer.play_previous: + - service: dfplayer_play + variables: + file: int + then: + - dfplayer.play: !lambda 'return file;' + - service: dfplayer_play_loop + variables: + file: int + loop_: bool + then: + - dfplayer.play: + file: !lambda 'return file;' + loop: !lambda 'return loop_;' + - service: dfplayer_play_folder + variables: + folder: int + file: int + then: + - dfplayer.play_folder: + folder: !lambda 'return folder;' + file: !lambda 'return file;' + + - service: dfplayer_play_loo_folder + variables: + folder: int + then: + - dfplayer.play_folder: + folder: !lambda 'return folder;' + loop: True + + - service: dfplayer_set_device + variables: + device: int + then: + - dfplayer.set_device: + device: TF_CARD + + - service: dfplayer_set_volume + variables: + volume: int + then: + - dfplayer.set_volume: !lambda 'return volume;' + - service: dfplayer_set_eq + variables: + preset: int + then: + - dfplayer.set_eq: !lambda 'return static_cast<dfplayer::EqPreset>(preset);' + + - service: dfplayer_sleep + then: + - dfplayer.sleep + + - service: dfplayer_reset + then: + - dfplayer.reset + + - service: dfplayer_start + then: + - dfplayer.start + + - service: dfplayer_pause + then: + - dfplayer.pause + + - service: dfplayer_stop + then: + - dfplayer.stop + + - service: dfplayer_random + then: + - dfplayer.random wifi: ssid: 'MySSID' @@ -115,6 +212,14 @@ sensor: - calibrate_linear: - 0 -> 0 - 100 -> 100 + - calibrate_polynomial: + degree: 3 + datapoints: + - 0 -> 0 + - 100 -> 200 + - 400 -> 500 + - -50 -> -1000 + - -100 -> -10000 - platform: resistance sensor: my_sensor configuration: DOWNSTREAM @@ -175,6 +280,42 @@ sensor: value: 15.0 - binary_sensor: bin3 value: 100.0 + - platform: ade7953 + voltage: + name: ADE7953 Voltage + current_a: + name: ADE7953 Current A + current_b: + name: ADE7953 Current B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B + - platform: pzem004t + voltage: + name: "PZEM00T Voltage" + current: + name: "PZEM004T Current" + power: + name: "PZEM004T Power" + - platform: pzemac + voltage: + name: "PZEMAC Voltage" + current: + name: "PZEMAC Current" + power: + name: "PZEMAC Power" + frequency: + name: "PZEMAC Frequency" + power_factor: + name: "PZEMAC Power Factor" + - platform: pzemdc + voltage: + name: "PZEMDC Voltage" + current: + name: "PZEMDC Current" + power: + name: "PZEMDC Power" time: - platform: homeassistant @@ -246,6 +387,11 @@ binary_sensor: - id: custom_binary_sensor name: Custom Binary Sensor +globals: + - id: my_global_string + type: std::string + initial_value: '""' + remote_receiver: pin: GPIO12 dump: [] @@ -262,6 +408,8 @@ text_sensor: - lambda: !lambda |- ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str()); - script.execute: my_script + - script.wait: my_script + - script.stop: my_script - homeassistant.service: service: notify.html5 data: @@ -300,12 +448,19 @@ switch: - platform: gpio id: gpio_switch1 pin: - mcp23017: mcp + mcp23017: mcp23017_hub number: 0 mode: OUTPUT - interlock: &interlock [gpio_switch1, gpio_switch2] + interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3] - platform: gpio id: gpio_switch2 + pin: + mcp23008: mcp23008_hub + number: 0 + mode: OUTPUT + interlock: *interlock + - platform: gpio + id: gpio_switch3 pin: GPIO1 interlock: *interlock - platform: custom @@ -416,6 +571,14 @@ cover: close_action: - switch.turn_on: gpio_switch2 close_duration: 4.5min + - platform: template + name: Template Cover with Tilt + tilt_lambda: "return 0.5;" + tilt_action: + - output.set_level: + id: out + level: !lambda "return tilt;" + output: - platform: esp8266_pwm @@ -440,7 +603,10 @@ output: - id: custom_float mcp23017: - id: mcp + id: mcp23017_hub + +mcp23008: + id: mcp23008_hub light: - platform: neopixelbus @@ -460,3 +626,23 @@ ttp229_lsf: ttp229_bsf: sdo_pin: D0 scl_pin: D1 + +sim800l: + on_sms_received: + - lambda: |- + std::string str; + str = sender; + str = message; + - sim800l.send_sms: + message: 'hello you' + recipient: '+1234' + +dfplayer: + on_finished_playback: + then: + if: + condition: + not: + dfplayer.is_playing + then: + logger.log: 'Playback finished event'